diff --git a/AndroidManifest.xml b/AndroidManifest.xml
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -240,6 +240,12 @@
android:value="org.kde.kdeconnect.Plugins.SharePlugin.ShareChooserTargetService" />
+
+
+
+
+
+
+ *
+ * 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.support.v4.content.FileProvider;
+import android.support.v4.provider.DocumentFile;
+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;
+
+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 Object lock; //Use to protect concurrent access to the variables below
+ private final List networkPacketList;
+ private int totalNumFiles;
+ private long totalPayloadSize;
+
+ CompositeReceiveFileJob(Device device, BackgroundJob.Callback callBack) {
+ super(device, callBack);
+
+ lock = new Object();
+ networkPacketList = new ArrayList<>();
+ shareNotification = new ShareNotification(device);
+ shareNotification.setJobId(getId());
+ currentFileNum = 0;
+ totalNumFiles = 0;
+ totalPayloadSize = 0;
+ totalReceived = 0;
+ lastProgressTimeMillis = 0;
+ prevProgressPercentage = 0;
+ }
+
+ private Device getDevice() {
+ return requestInfo;
+ }
+
+ void addNetworkPacket(NetworkPacket networkPacket) {
+ if (!networkPacketList.contains(networkPacket)) {
+ synchronized (lock) {
+ networkPacketList.add(networkPacket);
+
+ totalNumFiles = networkPacket.getInt(SharePlugin.KEY_NUMBER_OF_FILES, 1);
+ totalPayloadSize = networkPacket.getLong(SharePlugin.KEY_TOTAL_PAYLOAD_SIZE);
+
+ 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;
+
+ 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(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();
+
+ 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 && !canceled) {
+ try {
+ Thread.sleep(250);
+ } 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();
+ }
+ }
+
+ if (canceled) {
+ Log.e("ERIK", "I've been cancelled");
+ 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(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_title, numFiles, numFiles, getDevice().getName()));
+
+ if (totalNumFiles == 1 && fileDocument != null) {
+ shareNotification.setURI(fileDocument.getUri(), fileDocument.getType(), fileDocument.getName());
+ }
+
+ shareNotification.show();
+ }
+ reportResult(null);
+ } catch (Exception e) {
+ int failedFiles;
+ synchronized (lock) {
+ failedFiles = (totalNumFiles - currentFileNum + 1);
+ }
+ shareNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, failedFiles, totalNumFiles, getDevice().getName()));
+ shareNotification.show();
+ reportError(e);
+ } finally {
+ Log.e("ERIK", "Closing all input and output streams");
+ 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 || !ShareSettingsActivity.isCustomDestinationEnabled(getDevice().getContext())) {
+ final String defaultPath = ShareSettingsActivity.getDefaultDestinationDirectory().getAbsolutePath();
+ filenameToUse = FilesHelper.findNonExistingNameForNewFile(defaultPath, filenameToUse);
+ destinationFolderDocument = DocumentFile.fromFile(new File(defaultPath));
+ } else {
+ destinationFolderDocument = ShareSettingsActivity.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(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 && !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) {
+ Log.e("ERIK", "closing a networkPackets payload");
+ np.getPayload().close();
+ }
+ }
+
+ private void setProgress(int progress) {
+ synchronized (lock) {
+ 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 (!ShareSettingsActivity.isCustomDestinationEnabled(getDevice().getContext())) {
+ Log.i("SharePlugin", "Adding to downloads");
+ 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(getDevice().getContext(), fileDocument.getUri());
+ }
+ }
+
+ private void openFile(DocumentFile fileDocument) {
+ 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(getDevice().getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
+ intent.setDataAndType(contentUri, fileDocument.getType());
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ } else {
+ intent.setDataAndType(fileDocument.getUri(), fileDocument.getType());
+ }
+
+ getDevice().getContext().startActivity(intent);
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ReceiveFileRunnable.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ReceiveFileRunnable.java
deleted file mode 100644
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ReceiveFileRunnable.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.os.Handler;
-import android.os.Looper;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class ReceiveFileRunnable implements Runnable {
- interface CallBack {
- void onProgress(ShareInfo info, int progress);
- void onSuccess(ShareInfo info);
- void onError(ShareInfo info, Throwable error);
- }
-
- private final ShareInfo info;
- private final CallBack callBack;
- private final Handler handler;
-
- ReceiveFileRunnable(ShareInfo info, CallBack callBack) {
- this.info = info;
- this.callBack = callBack;
- this.handler = new Handler(Looper.getMainLooper());
- }
-
- @Override
- public void run() {
- try {
- byte data[] = new byte[4096];
- long received = 0, prevProgressPercentage = 0;
- int count;
-
- callBack.onProgress(info, 0);
-
- InputStream inputStream = info.payload.getInputStream();
-
- while ((count = inputStream.read(data)) >= 0) {
- received += count;
-
- if (received > info.fileSize) {
- break;
- }
-
- info.outputStream.write(data, 0, count);
- if (info.fileSize > 0) {
- long progressPercentage = (received * 100 / info.fileSize);
- if (progressPercentage != prevProgressPercentage) {
- prevProgressPercentage = progressPercentage;
- handler.post(() -> callBack.onProgress(info, (int)progressPercentage));
- }
- }
- //else Log.e("SharePlugin", "Infinite loop? :D");
- }
-
- info.outputStream.flush();
-
- if (received != info.fileSize) {
- throw new RuntimeException("Received:" + received + " bytes, expected: " + info.fileSize + " bytes");
- }
-
- handler.post(() -> callBack.onSuccess(info));
- } catch (IOException e) {
- handler.post(() -> callBack.onError(info, e));
- } finally {
- info.payload.close();
-
- try {
- info.outputStream.close();
- } catch (IOException ignored) {}
- }
- }
-}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java
@@ -146,7 +146,6 @@
final String deviceId = intent.getStringExtra("deviceId");
if (deviceId != null) {
-
BackgroundService.runWithPlugin(this, deviceId, SharePlugin.class, plugin -> {
plugin.share(intent);
finish();
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareBroadcastReceiver.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareBroadcastReceiver.java
new file mode 100644
--- /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
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareNotification.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareNotification.java
@@ -48,6 +48,7 @@
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;
@@ -73,7 +74,23 @@
notificationManager.cancel(notificationId);
}
- public int getId() {
+ public void setJobId(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;
}
diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java
--- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java
@@ -22,7 +22,6 @@
import android.Manifest;
import android.app.Activity;
-import android.app.DownloadManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -34,50 +33,50 @@
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
+import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
-import android.support.v4.content.FileProvider;
-import android.support.v4.provider.DocumentFile;
import android.util.Log;
import android.widget.Toast;
-import org.kde.kdeconnect.Helpers.FilesHelper;
-import org.kde.kdeconnect.Helpers.MediaStoreHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.UserInterface.DeviceSettingsActivity;
+import org.kde.kdeconnect.async.BackgroundJob;
+import org.kde.kdeconnect.async.BackgroundJobHandler;
import org.kde.kdeconnect_tp.R;
-import java.io.BufferedOutputStream;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-public class SharePlugin extends Plugin implements ReceiveFileRunnable.CallBack {
+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";
+ final static String KEY_NUMBER_OF_FILES = "numberOfFiles";
+ final static String KEY_TOTAL_PAYLOAD_SIZE = "totalPayloadSize";
+
private final static boolean openUrlsDirectly = true;
- private ShareNotification shareNotification;
- private FinishReceivingRunnable finishReceivingRunnable;
- private ExecutorService executorService;
- private ShareInfo currentShareInfo;
- private Handler handler;
+ private BackgroundJobHandler backgroundJobHandler;
+ private final Handler handler;
+ private CompositeReceiveFileJob receiveFileJob;
+ private final Callback receiveFileJobCallback;
public SharePlugin() {
- executorService = Executors.newSingleThreadExecutor();
+ backgroundJobHandler = BackgroundJobHandler.newFixedThreadPoolBackgroundJobHander(5);
handler = new Handler(Looper.getMainLooper());
+ receiveFileJobCallback = new Callback();
}
@Override
@@ -196,78 +195,29 @@
@WorkerThread
private void receiveFile(NetworkPacket np) {
- if (finishReceivingRunnable != null) {
- Log.i("SharePlugin", "receiveFile: canceling finishReceivingRunnable");
- handler.removeCallbacks(finishReceivingRunnable);
- finishReceivingRunnable = null;
- }
-
- ShareInfo info = new ShareInfo();
- info.currentFileNumber = currentShareInfo == null ? 1 : currentShareInfo.currentFileNumber + 1;
- info.payload = np.getPayload();
- info.fileSize = np.getPayloadSize();
- info.fileName = np.getString("filename", Long.toString(System.currentTimeMillis()));
- info.shouldOpen = np.getBoolean("open");
- info.setNumberOfFiles(np.getInt("numberOfFiles", 1));
- info.setTotalTransferSize(np.getLong("totalPayloadSize", 1));
-
- if (currentShareInfo == null) {
- currentShareInfo = info;
- } else {
- synchronized (currentShareInfo) {
- currentShareInfo.setNumberOfFiles(info.numberOfFiles());
- currentShareInfo.setTotalTransferSize(info.totalTransferSize());
- }
- }
+ CompositeReceiveFileJob job;
- String filename = info.fileName;
- final DocumentFile destinationFolderDocument;
+ boolean hasNumberOfFiles = np.has(KEY_NUMBER_OF_FILES);
+ boolean hasOpen = np.has("open");
- //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 (np.getBoolean("open") || !ShareSettingsActivity.isCustomDestinationEnabled(context)) {
- final String defaultPath = ShareSettingsActivity.getDefaultDestinationDirectory().getAbsolutePath();
- filename = FilesHelper.findNonExistingNameForNewFile(defaultPath, filename);
- destinationFolderDocument = DocumentFile.fromFile(new File(defaultPath));
+ if (hasNumberOfFiles && !hasOpen && receiveFileJob != null) {
+ job = receiveFileJob;
} else {
- destinationFolderDocument = ShareSettingsActivity.getDestinationDirectory(context);
+ job = new CompositeReceiveFileJob(device, receiveFileJobCallback);
}
- String displayName = FilesHelper.getFileNameWithoutExt(filename);
- String mimeType = FilesHelper.getMimeTypeFromFile(filename);
- if ("*/*".equals(mimeType)) {
- displayName = filename;
+ if (!hasNumberOfFiles) {
+ np.set(KEY_NUMBER_OF_FILES, 1);
+ np.set(KEY_TOTAL_PAYLOAD_SIZE, np.getPayloadSize());
}
- info.fileDocument = destinationFolderDocument.createFile(mimeType, displayName);
- assert info.fileDocument != null;
+ job.addNetworkPacket(np);
- if (shareNotification == null) {
- shareNotification = new ShareNotification(device);
- }
-
- if (info.fileDocument == null) {
- onError(info, new RuntimeException(context.getString(R.string.cannot_create_file, filename)));
- return;
- }
-
- shareNotification.setTitle(context.getResources().getQuantityString(R.plurals.incoming_file_title, info.numberOfFiles(), info.numberOfFiles(), device.getName()));
- shareNotification.show();
-
- if (np.hasPayload()) {
- try {
- info.outputStream = new BufferedOutputStream(context.getContentResolver().openOutputStream(info.fileDocument.getUri()));
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- return;
+ if (job != receiveFileJob) {
+ if (hasNumberOfFiles && !hasOpen) {
+ receiveFileJob = job;
}
-
- ReceiveFileRunnable runnable = new ReceiveFileRunnable(info, this);
- executorService.execute(runnable);
- } else {
- onProgress(info, 100);
- onSuccess(info);
+ backgroundJobHandler.runJob(job);
}
}
@@ -280,7 +230,6 @@
}
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) {
@@ -314,9 +263,7 @@
//Create the network package from the URI
private static NetworkPacket uriToNetworkPacket(final Context context, final Uri uri) {
-
try {
-
ContentResolver cr = context.getContentResolver();
InputStream inputStream = cr.openInputStream(uri);
@@ -376,7 +323,6 @@
} catch (Exception e) {
}
}
-
}
np.setPayload(new NetworkPacket.Payload(inputStream, size));
@@ -406,7 +352,6 @@
}
queuedSendUriList(uriList);
-
} catch (Exception e) {
Log.e("ShareActivity", "Exception");
e.printStackTrace();
@@ -440,7 +385,6 @@
device.sendPacket(np);
}
}
-
}
@Override
@@ -458,92 +402,29 @@
return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
}
- @Override
- public void onProgress(ShareInfo info, int progress) {
- if (progress == 0 && currentShareInfo != info) {
- currentShareInfo = info;
- }
-
- shareNotification.setProgress(progress, context.getResources().getQuantityString(R.plurals.incoming_files_text, info.numberOfFiles(), info.fileName, info.currentFileNumber, info.numberOfFiles()));
- shareNotification.show();
- }
-
- @Override
- public void onSuccess(ShareInfo info) {
- Log.i("SharePlugin", "onSuccess() - Transfer finished for file: " + info.fileDocument.getUri().getPath());
-
- if (info.shouldOpen) {
- shareNotification.cancel();
-
- 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(info.fileDocument.getUri().getPath());
- Uri contentUri = FileProvider.getUriForFile(device.getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
- intent.setDataAndType(contentUri, info.fileDocument.getType());
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- } else {
- intent.setDataAndType(info.fileDocument.getUri(), info.fileDocument.getType());
- }
-
- context.startActivity(intent);
- } else {
- if (!ShareSettingsActivity.isCustomDestinationEnabled(context)) {
- Log.i("SharePlugin", "Adding to downloads");
- DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
- manager.addCompletedDownload(info.fileDocument.getUri().getLastPathSegment(), device.getName(), true, info.fileDocument.getType(), info.fileDocument.getUri().getPath(), info.fileSize, false);
- } else {
- //Make sure it is added to the Android Gallery anyway
- MediaStoreHelper.indexFile(context, info.fileDocument.getUri());
+ private class Callback implements CompositeReceiveFileJob.Callback {
+ @Override
+ public void onResult(@NonNull BackgroundJob job, Void result) {
+ if (job == receiveFileJob) {
+ receiveFileJob = null;
}
+ }
- if (info.numberOfFiles() == 1 || info.currentFileNumber == info.numberOfFiles()) {
- finishReceivingRunnable = new FinishReceivingRunnable(info);
- Log.i("SharePlugin", "onSuccess() - scheduling finishReceivingRunnable");
- handler.postDelayed(finishReceivingRunnable, 1000);
+ @Override
+ public void onError(@NonNull BackgroundJob job, @NonNull Throwable error) {
+ if (job == receiveFileJob) {
+ receiveFileJob = null;
}
}
}
- @Override
- public void onError(ShareInfo info, Throwable error) {
- Log.e("SharePlugin", "onError: " + error.getMessage());
-
- info.fileDocument.delete();
-
- //TODO: Show error in notification
- int failedFiles = info.numberOfFiles() - (info.currentFileNumber - 1);
- shareNotification.setFinished(context.getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, failedFiles, info.numberOfFiles(), device.getName()));
- shareNotification.show();
- shareNotification = null;
- currentShareInfo = null;
- }
-
- private class FinishReceivingRunnable implements Runnable {
- private final ShareInfo info;
-
- private FinishReceivingRunnable(ShareInfo info) {
- this.info = info;
- }
-
- @Override
- public void run() {
- Log.i("SharePlugin", "FinishReceivingRunnable: Finishing up");
-
- if (shareNotification != null) {
- //Update the notification and allow to open the file from it
- shareNotification.setFinished(context.getResources().getQuantityString(R.plurals.received_files_title, info.numberOfFiles(), info.numberOfFiles(), device.getName()));
+ void cancelJob(long jobId) {
+ if (backgroundJobHandler.isRunning(jobId)) {
+ BackgroundJob job = backgroundJobHandler.getJob(jobId);
- if (info.numberOfFiles() == 1) {
- shareNotification.setURI(info.fileDocument.getUri(), info.fileDocument.getType(), info.fileName);
- }
-
- shareNotification.show();
- shareNotification = null;
+ if (job != null) {
+ job.cancel();
}
-
- finishReceivingRunnable = null;
- currentShareInfo = null;
}
}
}
diff --git a/src/org/kde/kdeconnect/async/BackgroundJob.java b/src/org/kde/kdeconnect/async/BackgroundJob.java
new file mode 100644
--- /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 android.support.annotation.NonNull;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+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
--- /dev/null
+++ b/src/org/kde/kdeconnect/async/BackgroundJobHandler.java
@@ -0,0 +1,169 @@
+/*
+ * 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.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.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+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<>());
+ }
+}