diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a435c0cf..f250abe6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,264 +1,270 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/CompositeReceiveFileRunnable.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/CompositeReceiveFileJob.java
similarity index 79%
rename from src/org/kde/kdeconnect/Plugins/SharePlugin/CompositeReceiveFileRunnable.java
rename to src/org/kde/kdeconnect/Plugins/SharePlugin/CompositeReceiveFileJob.java
index 7655b214..e4da572b 100644
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/CompositeReceiveFileRunnable.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/CompositeReceiveFileJob.java
@@ -1,327 +1,329 @@
/*
* 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.Plugins.SharePlugin;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.FilesHelper;
import org.kde.kdeconnect.Helpers.MediaStoreHelper;
import org.kde.kdeconnect.NetworkPacket;
+import org.kde.kdeconnect.async.BackgroundJob;
import org.kde.kdeconnect_tp.R;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import androidx.core.content.FileProvider;
import androidx.documentfile.provider.DocumentFile;
-class CompositeReceiveFileRunnable implements Runnable {
- interface CallBack {
- void onSuccess(CompositeReceiveFileRunnable runnable);
- void onError(CompositeReceiveFileRunnable runnable, Throwable error);
- }
-
- private final Device device;
+public class CompositeReceiveFileJob extends BackgroundJob {
private final ShareNotification shareNotification;
private NetworkPacket currentNetworkPacket;
private String currentFileName;
private int currentFileNum;
private long totalReceived;
private long lastProgressTimeMillis;
private long prevProgressPercentage;
- private final CallBack callBack;
- private final Handler handler;
-
private final Object lock; //Use to protect concurrent access to the variables below
private final List networkPacketList;
private int totalNumFiles;
private long totalPayloadSize;
private boolean isRunning;
- CompositeReceiveFileRunnable(Device device, CallBack callBack) {
- this.device = device;
- this.callBack = callBack;
+ CompositeReceiveFileJob(Device device, BackgroundJob.Callback callBack) {
+ super(device, callBack);
lock = new Object();
networkPacketList = new ArrayList<>();
shareNotification = new ShareNotification(device);
+ shareNotification.addCancelAction(getId());
currentFileNum = 0;
totalNumFiles = 0;
totalPayloadSize = 0;
totalReceived = 0;
lastProgressTimeMillis = 0;
prevProgressPercentage = 0;
- handler = new Handler(Looper.getMainLooper());
+ }
+
+ private Device getDevice() {
+ return requestInfo;
}
boolean isRunning() { return isRunning; }
void updateTotals(int numberOfFiles, long totalPayloadSize) {
synchronized (lock) {
this.totalNumFiles = numberOfFiles;
this.totalPayloadSize = totalPayloadSize;
- shareNotification.setTitle(device.getContext().getResources()
- .getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, device.getName()));
+ shareNotification.setTitle(getDevice().getContext().getResources()
+ .getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName()));
}
}
void addNetworkPacket(NetworkPacket networkPacket) {
synchronized (lock) {
if (!networkPacketList.contains(networkPacket)) {
networkPacketList.add(networkPacket);
totalNumFiles = networkPacket.getInt(SharePlugin.KEY_NUMBER_OF_FILES, 1);
totalPayloadSize = networkPacket.getLong(SharePlugin.KEY_TOTAL_PAYLOAD_SIZE);
- shareNotification.setTitle(device.getContext().getResources()
- .getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, device.getName()));
+ shareNotification.setTitle(getDevice().getContext().getResources()
+ .getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName()));
}
}
}
@Override
public void run() {
boolean done;
OutputStream outputStream = null;
synchronized (lock) {
done = networkPacketList.isEmpty();
}
try {
DocumentFile fileDocument = null;
isRunning = true;
- while (!done) {
+ while (!done && !canceled) {
synchronized (lock) {
currentNetworkPacket = networkPacketList.get(0);
}
currentFileName = currentNetworkPacket.getString("filename", Long.toString(System.currentTimeMillis()));
currentFileNum++;
setProgress((int)prevProgressPercentage);
fileDocument = getDocumentFileFor(currentFileName, currentNetworkPacket.getBoolean("open"));
if (currentNetworkPacket.hasPayload()) {
- outputStream = new BufferedOutputStream(device.getContext().getContentResolver().openOutputStream(fileDocument.getUri()));
+ outputStream = new BufferedOutputStream(getDevice().getContext().getContentResolver().openOutputStream(fileDocument.getUri()));
InputStream inputStream = currentNetworkPacket.getPayload().getInputStream();
long received = receiveFile(inputStream, outputStream);
currentNetworkPacket.getPayload().close();
if ( received != currentNetworkPacket.getPayloadSize()) {
fileDocument.delete();
- throw new RuntimeException("Failed to receive: " + currentFileName + " received:" + received + " bytes, expected: " + currentNetworkPacket.getPayloadSize() + " bytes");
+
+ if (!canceled) {
+ throw new RuntimeException("Failed to receive: " + currentFileName + " received:" + received + " bytes, expected: " + currentNetworkPacket.getPayloadSize() + " bytes");
+ }
} else {
publishFile(fileDocument, received);
}
} else {
setProgress(100);
publishFile(fileDocument, 0);
}
boolean listIsEmpty;
synchronized (lock) {
networkPacketList.remove(0);
listIsEmpty = networkPacketList.isEmpty();
}
- if (listIsEmpty) {
+ if (listIsEmpty && !canceled) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
synchronized (lock) {
if (currentFileNum < totalNumFiles && networkPacketList.isEmpty()) {
throw new RuntimeException("Failed to receive " + (totalNumFiles - currentFileNum + 1) + " files");
}
}
}
synchronized (lock) {
done = networkPacketList.isEmpty();
}
}
isRunning = false;
+ if (canceled) {
+ shareNotification.cancel();
+ return;
+ }
+
int numFiles;
synchronized (lock) {
numFiles = totalNumFiles;
}
if (numFiles == 1 && currentNetworkPacket.has("open")) {
shareNotification.cancel();
openFile(fileDocument);
} else {
//Update the notification and allow to open the file from it
- shareNotification.setFinished(device.getContext().getResources().getQuantityString(R.plurals.received_files_title, numFiles, device.getName(), numFiles));
+ shareNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_title, numFiles, getDevice().getName(), numFiles));
if (totalNumFiles == 1 && fileDocument != null) {
shareNotification.setURI(fileDocument.getUri(), fileDocument.getType(), fileDocument.getName());
}
shareNotification.show();
}
- handler.post(() -> callBack.onSuccess(this));
+ reportResult(null);
} catch (Exception e) {
isRunning = false;
int failedFiles;
synchronized (lock) {
failedFiles = (totalNumFiles - currentFileNum + 1);
}
- shareNotification.setFinished(device.getContext().getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, device.getName(), failedFiles, totalNumFiles));
+
+ shareNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, getDevice().getName(), failedFiles, totalNumFiles));
shareNotification.show();
- handler.post(() -> callBack.onError(this, e));
+ reportError(e);
} finally {
closeAllInputStreams();
networkPacketList.clear();
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ignored) {}
}
}
}
private DocumentFile getDocumentFileFor(final String filename, final boolean open) throws RuntimeException {
final DocumentFile destinationFolderDocument;
String filenameToUse = filename;
//We need to check for already existing files only when storing in the default path.
//User-defined paths use the new Storage Access Framework that already handles this.
//If the file should be opened immediately store it in the standard location to avoid the FileProvider trouble (See ShareNotification::setURI)
- if (open || !ShareSettingsFragment.isCustomDestinationEnabled(device.getContext())) {
+ if (open || !ShareSettingsFragment.isCustomDestinationEnabled(getDevice().getContext())) {
final String defaultPath = ShareSettingsFragment.getDefaultDestinationDirectory().getAbsolutePath();
filenameToUse = FilesHelper.findNonExistingNameForNewFile(defaultPath, filenameToUse);
destinationFolderDocument = DocumentFile.fromFile(new File(defaultPath));
} else {
- destinationFolderDocument = ShareSettingsFragment.getDestinationDirectory(device.getContext());
+ destinationFolderDocument = ShareSettingsFragment.getDestinationDirectory(getDevice().getContext());
}
String displayName = FilesHelper.getFileNameWithoutExt(filenameToUse);
String mimeType = FilesHelper.getMimeTypeFromFile(filenameToUse);
if ("*/*".equals(mimeType)) {
displayName = filenameToUse;
}
DocumentFile fileDocument = destinationFolderDocument.createFile(mimeType, displayName);
if (fileDocument == null) {
- throw new RuntimeException(device.getContext().getString(R.string.cannot_create_file, filenameToUse));
+ throw new RuntimeException(getDevice().getContext().getString(R.string.cannot_create_file, filenameToUse));
}
return fileDocument;
}
private long receiveFile(InputStream input, OutputStream output) throws IOException {
byte data[] = new byte[4096];
int count;
long received = 0;
- while ((count = input.read(data)) >= 0) {
+ while ((count = input.read(data)) >= 0 && !canceled) {
received += count;
totalReceived += count;
output.write(data, 0, count);
long progressPercentage;
synchronized (lock) {
progressPercentage = (totalReceived * 100 / totalPayloadSize);
}
long curTimeMillis = System.currentTimeMillis();
if (progressPercentage != prevProgressPercentage &&
(progressPercentage == 100 || curTimeMillis - lastProgressTimeMillis >= 500)) {
prevProgressPercentage = progressPercentage;
lastProgressTimeMillis = curTimeMillis;
setProgress((int)progressPercentage);
}
}
output.flush();
return received;
}
private void closeAllInputStreams() {
for (NetworkPacket np : networkPacketList) {
np.getPayload().close();
}
}
private void setProgress(int progress) {
synchronized (lock) {
- shareNotification.setProgress(progress, device.getContext().getResources()
+ shareNotification.setProgress(progress, getDevice().getContext().getResources()
.getQuantityString(R.plurals.incoming_files_text, totalNumFiles, currentFileName, currentFileNum, totalNumFiles));
}
shareNotification.show();
}
private void publishFile(DocumentFile fileDocument, long size) {
- if (!ShareSettingsFragment.isCustomDestinationEnabled(device.getContext())) {
+ if (!ShareSettingsFragment.isCustomDestinationEnabled(getDevice().getContext())) {
Log.i("SharePlugin", "Adding to downloads");
- DownloadManager manager = (DownloadManager) device.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
- manager.addCompletedDownload(fileDocument.getUri().getLastPathSegment(), device.getName(), true, fileDocument.getType(), fileDocument.getUri().getPath(), size, false);
+ DownloadManager manager = (DownloadManager) getDevice().getContext().getSystemService(Context.DOWNLOAD_SERVICE);
+ manager.addCompletedDownload(fileDocument.getUri().getLastPathSegment(), getDevice().getName(), true, fileDocument.getType(), fileDocument.getUri().getPath(), size, false);
} else {
//Make sure it is added to the Android Gallery anyway
Log.i("SharePlugin", "Adding to gallery");
- MediaStoreHelper.indexFile(device.getContext(), fileDocument.getUri());
+ MediaStoreHelper.indexFile(getDevice().getContext(), fileDocument.getUri());
}
}
private void openFile(DocumentFile fileDocument) {
String mimeType = FilesHelper.getMimeTypeFromFile(fileDocument.getName());
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= 24) {
//Nougat and later require "content://" uris instead of "file://" uris
File file = new File(fileDocument.getUri().getPath());
- Uri contentUri = FileProvider.getUriForFile(device.getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
+ Uri contentUri = FileProvider.getUriForFile(getDevice().getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
intent.setDataAndType(contentUri, mimeType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
intent.setDataAndType(fileDocument.getUri(), mimeType);
}
- device.getContext().startActivity(intent);
+ getDevice().getContext().startActivity(intent);
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
index 3966b100..f93e6bfa 100644
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
@@ -1,174 +1,173 @@
/*
* Copyright 2014 Albert Vaca Cintora
*
* 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.Plugins.SharePlugin;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ListView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.UserInterface.List.EntryItem;
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.List.SectionItem;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
public class ShareActivity extends AppCompatActivity {
private SwipeRefreshLayout mSwipeRefreshLayout;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.refresh, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh:
updateComputerListAction();
break;
default:
break;
}
return true;
}
private void updateComputerListAction() {
updateComputerList();
BackgroundService.RunCommand(ShareActivity.this, BackgroundService::onNetworkChange);
mSwipeRefreshLayout.setRefreshing(true);
new Thread(() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
}
runOnUiThread(() -> mSwipeRefreshLayout.setRefreshing(false));
}).start();
}
private void updateComputerList() {
final Intent intent = getIntent();
String action = intent.getAction();
if (!Intent.ACTION_SEND.equals(action) && !Intent.ACTION_SEND_MULTIPLE.equals(action)) {
finish();
return;
}
BackgroundService.RunCommand(this, service -> {
Collection devices = service.getDevices().values();
final ArrayList devicesList = new ArrayList<>();
final ArrayList items = new ArrayList<>();
SectionItem section = new SectionItem(getString(R.string.share_to));
items.add(section);
for (Device d : devices) {
if (d.isReachable() && d.isPaired()) {
devicesList.add(d);
items.add(new EntryItem(d.getName()));
section.isEmpty = false;
}
}
runOnUiThread(() -> {
ListView list = findViewById(R.id.devices_list);
list.setAdapter(new ListAdapter(ShareActivity.this, items));
list.setOnItemClickListener((adapterView, view, i, l) -> {
Device device = devicesList.get(i - 1); //NOTE: -1 because of the title!
BackgroundService.runWithPlugin(this, device.getDeviceId(), SharePlugin.class, plugin -> plugin.share(intent));
finish();
});
});
});
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeUtil.setUserPreferredTheme(this);
setContentView(R.layout.devices_list);
ActionBar actionBar = getSupportActionBar();
mSwipeRefreshLayout = findViewById(R.id.refresh_list_layout);
mSwipeRefreshLayout.setOnRefreshListener(
this::updateComputerListAction
);
if (actionBar != null) {
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
}
}
@Override
protected void onStart() {
super.onStart();
final Intent intent = getIntent();
final String deviceId = intent.getStringExtra("deviceId");
if (deviceId != null) {
-
BackgroundService.runWithPlugin(this, deviceId, SharePlugin.class, plugin -> {
plugin.share(intent);
finish();
});
} else {
BackgroundService.addGuiInUseCounter(this);
BackgroundService.RunCommand(this, service -> {
service.onNetworkChange();
service.addDeviceListChangedCallback("ShareActivity", this::updateComputerList);
});
updateComputerList();
}
}
@Override
protected void onStop() {
BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("ShareActivity"));
BackgroundService.removeGuiInUseCounter(this);
super.onStop();
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareBroadcastReceiver.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareBroadcastReceiver.java
new file mode 100644
index 00000000..2c0d82f2
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareBroadcastReceiver.java
@@ -0,0 +1,57 @@
+/*
+ * 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.Plugins.SharePlugin;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import org.kde.kdeconnect.BackgroundService;
+
+public class ShareBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case SharePlugin.ACTION_CANCEL_SHARE:
+ cancelShare(context, intent);
+ break;
+ default:
+ Log.d("ShareBroadcastReceiver", "Unhandled Action received: " + intent.getAction());
+ }
+ }
+
+ private void cancelShare(Context context, Intent intent) {
+ if (!intent.hasExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA) ||
+ !intent.hasExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA)) {
+ Log.e("ShareBroadcastReceiver", "cancelShare() - not all expected extra's are present. Ignoring this cancel intent");
+ return;
+ }
+
+ long jobId = intent.getLongExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA, -1);
+ String deviceId = intent.getStringExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA);
+
+ BackgroundService.RunCommand(context, service -> {
+ SharePlugin plugin = service.getDevice(deviceId).getPlugin(SharePlugin.class);
+ plugin.cancelJob(jobId);
+ });
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareNotification.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareNotification.java
index cc08ae79..5870ccd4 100644
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareNotification.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareNotification.java
@@ -1,194 +1,211 @@
package org.kde.kdeconnect.Plugins.SharePlugin;
/*
* Copyright 2017 Nicolas Fella
*
* 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 .
*/
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect_tp.R;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import androidx.core.app.NotificationCompat;
import androidx.core.content.FileProvider;
class ShareNotification {
private final NotificationManager notificationManager;
private final int notificationId;
private NotificationCompat.Builder builder;
private final Device device;
+ private long currentJobId;
//https://documentation.onesignal.com/docs/android-customizations#section-big-picture
private static final int bigImageWidth = 1440;
private static final int bigImageHeight = 720;
public ShareNotification(Device device) {
this.device = device;
notificationId = (int) System.currentTimeMillis();
notificationManager = (NotificationManager) device.getContext().getSystemService(Context.NOTIFICATION_SERVICE);
builder = new NotificationCompat.Builder(device.getContext(), NotificationHelper.Channels.FILETRANSFER)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setAutoCancel(true)
.setOngoing(true)
.setProgress(100, 0, true);
}
public void show() {
NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build());
}
public void cancel() {
notificationManager.cancel(notificationId);
}
- public int getId() {
+ public void addCancelAction(long jobId) {
+ builder.mActions.clear();
+
+ currentJobId = jobId;
+ Intent cancelIntent = new Intent(device.getContext(), ShareBroadcastReceiver.class);
+ cancelIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ cancelIntent.setAction(SharePlugin.ACTION_CANCEL_SHARE);
+ cancelIntent.putExtra(SharePlugin.CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA, jobId);
+ cancelIntent.putExtra(SharePlugin.CANCEL_SHARE_DEVICE_ID_EXTRA, device.getDeviceId());
+ PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(device.getContext(), 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ builder.addAction(R.drawable.ic_reject_pairing, device.getContext().getString(R.string.cancel), cancelPendingIntent);
+ }
+
+ public long getCurrentJobId() { return currentJobId; }
+
+ public int getNotificationId() {
return notificationId;
}
public void setTitle(String title) {
builder.setContentTitle(title);
builder.setTicker(title);
}
public void setProgress(int progress, String progressMessage) {
builder.setProgress( 100, progress, false);
builder.setContentText(progressMessage);
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(progressMessage));
}
public void setFinished(String message) {
builder = new NotificationCompat.Builder(device.getContext(), NotificationHelper.Channels.DEFAULT);
builder.setContentTitle(message)
.setTicker(message)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setAutoCancel(true)
.setOngoing(false);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(device.getContext());
if (prefs.getBoolean("share_notification_preference", true)) {
builder.setDefaults(Notification.DEFAULT_ALL);
}
}
public void setURI(Uri destinationUri, String mimeType, String filename) {
/*
* We only support file URIs (because sending a content uri to another app does not work for security reasons).
* In effect, that means only the default download folder currently works.
*
* TODO: implement our own content provider (instead of support-v4's FileProvider). It should:
* - Proxy to real files (in case of the default download folder)
* - Proxy to the underlying content uri (in case of a custom download folder)
*/
//If it's an image, try to show it in the notification
if (mimeType.startsWith("image/")) {
//https://developer.android.com/topic/performance/graphics/load-bitmap
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
try (InputStream decodeBoundsInputStream = device.getContext().getContentResolver().openInputStream(destinationUri);
InputStream decodeInputStream = device.getContext().getContentResolver().openInputStream(destinationUri)) {
BitmapFactory.decodeStream(decodeBoundsInputStream, null, options);
options.inJustDecodeBounds = false;
options.inSampleSize = calculateInSampleSize(options, bigImageWidth, bigImageHeight);
Bitmap image = BitmapFactory.decodeStream(decodeInputStream, null, options);
if (image != null) {
builder.setLargeIcon(image);
builder.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(image));
}
} catch (IOException ignored) {
}
}
if (!"file".equals(destinationUri.getScheme())) {
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType(mimeType);
if (Build.VERSION.SDK_INT >= 24) {
//Nougat and later require "content://" uris instead of "file://" uris
File file = new File(destinationUri.getPath());
Uri contentUri = FileProvider.getUriForFile(device.getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
intent.setDataAndType(contentUri, mimeType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
} else {
intent.setDataAndType(destinationUri, mimeType);
shareIntent.putExtra(Intent.EXTRA_STREAM, destinationUri);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent resultPendingIntent = PendingIntent.getActivity(
device.getContext(),
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
);
builder.setContentText(device.getContext().getResources().getString(R.string.received_file_text, filename))
.setContentIntent(resultPendingIntent);
shareIntent = Intent.createChooser(shareIntent,
device.getContext().getString(R.string.share_received_file, destinationUri.getLastPathSegment()));
PendingIntent sharePendingIntent = PendingIntent.getActivity(device.getContext(), (int) System.currentTimeMillis(),
shareIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action.Builder shareAction = new NotificationCompat.Action.Builder(
R.drawable.ic_share_white, device.getContext().getString(R.string.share), sharePendingIntent);
builder.addAction(shareAction.build());
}
private int calculateInSampleSize(BitmapFactory.Options options, int targetWidth, int targetHeight) {
int inSampleSize = 1;
if (options.outHeight > targetHeight || options.outWidth > targetWidth) {
final int halfHeight = options.outHeight / 2;
final int halfWidth = options.outWidth / 2;
while ((halfHeight / inSampleSize) >= targetHeight
&& (halfWidth / inSampleSize) >= targetWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java
index 809f8580..0858e2e3 100644
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java
@@ -1,352 +1,368 @@
/*
* Copyright 2014 Albert Vaca Cintora
*
* 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.Plugins.SharePlugin;
import android.Manifest;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import org.kde.kdeconnect.Helpers.FilesHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
+import org.kde.kdeconnect.async.BackgroundJob;
+import org.kde.kdeconnect.async.BackgroundJobHandler;
import org.kde.kdeconnect_tp.R;
import java.net.URL;
import java.util.ArrayList;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
@PluginFactory.LoadablePlugin
public class SharePlugin extends Plugin {
+ final static String ACTION_CANCEL_SHARE = "org.kde.kdeconnect.Plugins.SharePlugin.CancelShare";
+ final static String CANCEL_SHARE_DEVICE_ID_EXTRA = "deviceId";
+ final static String CANCEL_SHARE_BACKGROUND_JOB_ID_EXTRA = "backgroundJobId";
+
private final static String PACKET_TYPE_SHARE_REQUEST = "kdeconnect.share.request";
private final static String PACKET_TYPE_SHARE_REQUEST_UPDATE = "kdeconnect.share.request.update";
final static String KEY_NUMBER_OF_FILES = "numberOfFiles";
final static String KEY_TOTAL_PAYLOAD_SIZE = "totalPayloadSize";
private final static boolean openUrlsDirectly = true;
- private ExecutorService executorService;
+ private BackgroundJobHandler backgroundJobHandler;
private final Handler handler;
- private CompositeReceiveFileRunnable receiveFileRunnable;
- private final Callback receiveFileRunnableCallback;
+
+ private CompositeReceiveFileJob receiveFileJob;
+ private final Callback receiveFileJobCallback;
public SharePlugin() {
- executorService = Executors.newFixedThreadPool(5);
+ backgroundJobHandler = BackgroundJobHandler.newFixedThreadPoolBackgroundJobHander(5);
handler = new Handler(Looper.getMainLooper());
- receiveFileRunnableCallback = new Callback();
+ receiveFileJobCallback = new Callback();
}
@Override
public boolean onCreate() {
optionalPermissionExplanation = R.string.share_optional_permission_explanation;
return true;
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_sharereceiver);
}
@Override
public Drawable getIcon() {
return ContextCompat.getDrawable(context, R.drawable.share_plugin_action);
}
@Override
public String getDescription() {
return context.getResources().getString(R.string.pref_plugin_sharereceiver_desc);
}
@Override
public boolean hasMainActivity() {
return true;
}
@Override
public String getActionName() {
return context.getString(R.string.send_files);
}
@Override
public void startMainActivity(Activity parentActivity) {
Intent intent = new Intent(parentActivity, SendFileActivity.class);
intent.putExtra("deviceId", device.getDeviceId());
parentActivity.startActivity(intent);
}
@Override
public boolean hasSettings() {
return true;
}
@Override
@WorkerThread
public boolean onPacketReceived(NetworkPacket np) {
try {
if (np.getType().equals(PACKET_TYPE_SHARE_REQUEST_UPDATE)) {
- if (receiveFileRunnable != null && receiveFileRunnable.isRunning()) {
- receiveFileRunnable.updateTotals(np.getInt(KEY_NUMBER_OF_FILES), np.getLong(KEY_TOTAL_PAYLOAD_SIZE));
+ if (receiveFileJob != null && receiveFileJob.isRunning()) {
+ receiveFileJob.updateTotals(np.getInt(KEY_NUMBER_OF_FILES), np.getLong(KEY_TOTAL_PAYLOAD_SIZE));
} else {
Log.d("SharePlugin", "Received update packet but CompositeUploadJob is null or not running");
}
return true;
}
if (np.has("filename")) {
if (isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
receiveFile(np);
} else {
Log.i("SharePlugin", "no Permission for Storage");
}
} else if (np.has("text")) {
Log.i("SharePlugin", "hasText");
receiveText(np);
} else if (np.has("url")) {
receiveUrl(np);
} else {
Log.e("SharePlugin", "Error: Nothing attached!");
}
} catch (Exception e) {
Log.e("SharePlugin", "Exception");
e.printStackTrace();
}
return true;
}
private void receiveUrl(NetworkPacket np) {
String url = np.getString("url");
Log.i("SharePlugin", "hasUrl: " + url);
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (openUrlsDirectly) {
context.startActivity(browserIntent);
} else {
Resources res = context.getResources();
PendingIntent resultPendingIntent = PendingIntent.getActivity(
context,
0,
browserIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification noti = new NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
.setContentTitle(res.getString(R.string.received_url_title, device.getName()))
.setContentText(res.getString(R.string.received_url_text, url))
.setContentIntent(resultPendingIntent)
.setTicker(res.getString(R.string.received_url_title, device.getName()))
.setSmallIcon(R.drawable.ic_notification)
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL)
.build();
NotificationHelper.notifyCompat(notificationManager, (int) System.currentTimeMillis(), noti);
}
}
private void receiveText(NetworkPacket np) {
String text = np.getString("text");
ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
cm.setText(text);
handler.post(() -> Toast.makeText(context, R.string.shareplugin_text_saved, Toast.LENGTH_LONG).show());
}
@WorkerThread
private void receiveFile(NetworkPacket np) {
- CompositeReceiveFileRunnable runnable;
+ CompositeReceiveFileJob job;
boolean hasNumberOfFiles = np.has(KEY_NUMBER_OF_FILES);
boolean hasOpen = np.has("open");
- if (hasNumberOfFiles && !hasOpen && receiveFileRunnable != null) {
- runnable = receiveFileRunnable;
+ if (hasNumberOfFiles && !hasOpen && receiveFileJob != null) {
+ job = receiveFileJob;
} else {
- runnable = new CompositeReceiveFileRunnable(device, receiveFileRunnableCallback);
+ job = new CompositeReceiveFileJob(device, receiveFileJobCallback);
}
if (!hasNumberOfFiles) {
np.set(KEY_NUMBER_OF_FILES, 1);
np.set(KEY_TOTAL_PAYLOAD_SIZE, np.getPayloadSize());
}
- runnable.addNetworkPacket(np);
+ job.addNetworkPacket(np);
- if (runnable != receiveFileRunnable) {
+ if (job != receiveFileJob) {
if (hasNumberOfFiles && !hasOpen) {
- receiveFileRunnable = runnable;
+ receiveFileJob = job;
}
- executorService.execute(runnable);
+ backgroundJobHandler.runJob(job);
}
}
@Override
public PluginSettingsFragment getSettingsFragment(Activity activity) {
return ShareSettingsFragment.newInstance(getPluginKey());
}
void queuedSendUriList(final ArrayList uriList) {
-
//Read all the data early, as we only have permissions to do it while the activity is alive
final ArrayList toSend = new ArrayList<>();
for (Uri uri : uriList) {
NetworkPacket np = FilesHelper.uriToNetworkPacket(context, uri, PACKET_TYPE_SHARE_REQUEST);
if (np != null) {
toSend.add(np);
}
}
//Callback that shows a progress notification
final NotificationUpdateCallback notificationUpdateCallback = new NotificationUpdateCallback(context, device, toSend);
//Do the sending in background
new Thread(() -> {
//Actually send the files
try {
for (NetworkPacket np : toSend) {
boolean success = device.sendPacketBlocking(np, notificationUpdateCallback);
if (!success) {
Log.e("SharePlugin", "Error sending files");
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
public void share(Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
if (extras.containsKey(Intent.EXTRA_STREAM)) {
try {
ArrayList uriList;
if (!Intent.ACTION_SEND.equals(intent.getAction())) {
uriList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
} else {
Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
uriList = new ArrayList<>();
uriList.add(uri);
}
queuedSendUriList(uriList);
-
} catch (Exception e) {
Log.e("ShareActivity", "Exception");
e.printStackTrace();
}
} else if (extras.containsKey(Intent.EXTRA_TEXT)) {
String text = extras.getString(Intent.EXTRA_TEXT);
String subject = extras.getString(Intent.EXTRA_SUBJECT);
//Hack: Detect shared youtube videos, so we can open them in the browser instead of as text
if (subject != null && subject.endsWith("YouTube")) {
int index = text.indexOf(": http://youtu.be/");
if (index > 0) {
text = text.substring(index + 2); //Skip ": "
}
}
boolean isUrl;
try {
new URL(text);
isUrl = true;
} catch (Exception e) {
isUrl = false;
}
NetworkPacket np = new NetworkPacket(SharePlugin.PACKET_TYPE_SHARE_REQUEST);
if (isUrl) {
np.set("url", text);
} else {
np.set("text", text);
}
device.sendPacket(np);
}
}
-
}
@Override
public String[] getSupportedPacketTypes() {
return new String[]{PACKET_TYPE_SHARE_REQUEST, PACKET_TYPE_SHARE_REQUEST_UPDATE};
}
@Override
public String[] getOutgoingPacketTypes() {
return new String[]{PACKET_TYPE_SHARE_REQUEST};
}
@Override
public String[] getOptionalPermissions() {
return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
}
- private class Callback implements CompositeReceiveFileRunnable.CallBack {
+ private class Callback implements CompositeReceiveFileJob.Callback {
@Override
- public void onSuccess(CompositeReceiveFileRunnable runnable) {
- if (runnable == receiveFileRunnable) {
- receiveFileRunnable = null;
+ public void onResult(@NonNull BackgroundJob job, Void result) {
+ if (job == receiveFileJob) {
+ receiveFileJob = null;
}
}
@Override
- public void onError(CompositeReceiveFileRunnable runnable, Throwable error) {
- Log.e("SharePlugin", "onError() - " + error.getMessage());
- if (runnable == receiveFileRunnable) {
- receiveFileRunnable = null;
+ public void onError(@NonNull BackgroundJob job, @NonNull Throwable error) {
+ if (job == receiveFileJob) {
+ receiveFileJob = null;
+ }
+ }
+ }
+
+ void cancelJob(long jobId) {
+ if (backgroundJobHandler.isRunning(jobId)) {
+ BackgroundJob job = backgroundJobHandler.getJob(jobId);
+
+ if (job != null) {
+ job.cancel();
+
+ if (job == receiveFileJob) {
+ receiveFileJob = null;
+ }
}
}
}
}
diff --git a/src/org/kde/kdeconnect/async/BackgroundJob.java b/src/org/kde/kdeconnect/async/BackgroundJob.java
new file mode 100644
index 00000000..168f297d
--- /dev/null
+++ b/src/org/kde/kdeconnect/async/BackgroundJob.java
@@ -0,0 +1,76 @@
+/*
+ * 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 java.util.concurrent.atomic.AtomicLong;
+
+import androidx.annotation.NonNull;
+
+public abstract class BackgroundJob implements Runnable {
+ private static AtomicLong atomicLong = new AtomicLong(0);
+ protected volatile boolean canceled;
+ private 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 job, R result);
+ void onError(@NonNull BackgroundJob job, @NonNull Throwable error);
+ }
+
+ protected void reportResult(R result) {
+ backgroundJobHandler.runOnUiThread(() -> {
+ callback.onResult(this, result);
+ backgroundJobHandler.onFinished(this);
+ });
+ }
+
+ 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
index 00000000..b2891833
--- /dev/null
+++ b/src/org/kde/kdeconnect/async/BackgroundJobHandler.java
@@ -0,0 +1,170 @@
+/*
+ * 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.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.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import androidx.annotation.Nullable;
+
+public class BackgroundJobHandler {
+ private static final String TAG = BackgroundJobHandler.class.getSimpleName();
+
+ private final Map> jobMap = new HashMap<>();
+ private final Object jobMapLock = new Object();
+
+ private class MyThreadPoolExecutor extends ThreadPoolExecutor {
+ MyThreadPoolExecutor(int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
+ super(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
+ }
+
+ @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 a CancellationException");
+ } 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 threadPoolExecutor;
+ private Handler handler;
+
+ private BackgroundJobHandler(int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
+ this.handler = new Handler(Looper.getMainLooper());
+ this.threadPoolExecutor = new MyThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
+ }
+
+ public void runJob(BackgroundJob bgJob) {
+ Future> f;
+
+ bgJob.setBackgroundJobHandler(this);
+
+ try {
+ synchronized (jobMapLock) {
+ f = threadPoolExecutor.submit(bgJob);
+ jobMap.put(bgJob, f);
+ }
+ } catch (RejectedExecutionException e) {
+ Log.d(TAG,"threadPoolExecutor.submit rejected a background job: " + e.getMessage());
+
+ bgJob.reportError(e);
+ }
+ }
+
+ 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;
+ }
+
+ void cancelJob(BackgroundJob job) {
+ synchronized (jobMapLock) {
+ if (jobMap.containsKey(job)) {
+ Future> f = jobMap.get(job);
+
+ if (f.cancel(true)) {
+ threadPoolExecutor.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);
+ }
+
+ public static BackgroundJobHandler newFixedThreadPoolBackgroundJobHander(int numThreads) {
+ return new BackgroundJobHandler(numThreads, numThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
+ }
+}