Commit f2e505b8 authored by Erik Duisters's avatar Erik Duisters

Allow shares from desktop to be canceled

Summary:
Allow in progress file transfers to be canceled

BUG: 349956

{F6373048}

{F6373050}

{F6373051}

Test Plan:
Send a large file from desktop to android
Press cancel in the progress notification

Result: the file transfer is cancelled and the cancelled file is deleted from storage

Reviewers: #kde_connect, nicolasfella, albertvaka

Reviewed By: #kde_connect, albertvaka

Subscribers: albertvaka, nicolasfella, kdeconnect

Tags: #kde_connect

Differential Revision: https://phabricator.kde.org/D16491
parent a6fdddf8
......@@ -224,6 +224,12 @@
android:value="org.kde.kdeconnect.Plugins.SharePlugin.ShareChooserTargetService" />
</activity>
<receiver android:name="org.kde.kdeconnect.Plugins.SharePlugin.ShareBroadcastReceiver">
<intent-filter>
<action android:name="org.kde.kdeconnect.Plugins.SharePlugin.CancelShare" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="org.kde.kdeconnect_tp.fileprovider"
......
......@@ -147,7 +147,6 @@ public class ShareActivity extends AppCompatActivity {
final String deviceId = intent.getStringExtra("deviceId");
if (deviceId != null) {
BackgroundService.runWithPlugin(this, deviceId, SharePlugin.class, plugin -> {
plugin.share(intent);
finish();
......
/*
* Copyright 2018 Erik Duisters <e.duisters1@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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);
});
}
}
......@@ -48,6 +48,7 @@ class ShareNotification {
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 @@ class ShareNotification {
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;
}
......
......@@ -43,19 +43,24 @@ 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";
......@@ -63,15 +68,16 @@ public class SharePlugin extends Plugin {
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
......@@ -122,8 +128,8 @@ public class SharePlugin extends Plugin {
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");
}
......@@ -200,15 +206,15 @@ public class SharePlugin extends Plugin {
@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) {
......@@ -216,13 +222,13 @@ public class SharePlugin extends Plugin {
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);
}
}
......@@ -232,7 +238,6 @@ public class SharePlugin extends Plugin {
}
void queuedSendUriList(final ArrayList<Uri> uriList) {
//Read all the data early, as we only have permissions to do it while the activity is alive
final ArrayList<NetworkPacket> toSend = new ArrayList<>();
for (Uri uri : uriList) {
......@@ -281,7 +286,6 @@ public class SharePlugin extends Plugin {
}
queuedSendUriList(uriList);
} catch (Exception e) {
Log.e("ShareActivity", "Exception");
e.printStackTrace();
......@@ -315,7 +319,6 @@ public class SharePlugin extends Plugin {
device.sendPacket(np);
}
}
}
@Override
......@@ -333,19 +336,32 @@ public class SharePlugin extends Plugin {
return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
}
private class Callback implements CompositeReceiveFileRunnable.CallBack {
private class Callback implements CompositeReceiveFileJob.Callback<Void> {
@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;
}
}
}
}
......
/*
* Copyright 2018 Erik Duisters <e.duisters1@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.async;
import java.util.concurrent.atomic.AtomicLong;
import androidx.annotation.NonNull;
public abstract class BackgroundJob<I, R> implements Runnable {
private static AtomicLong atomicLong = new AtomicLong(0);
protected volatile boolean canceled;
private BackgroundJobHandler backgroundJobHandler;
private long id;
protected I requestInfo;
private Callback<R> callback;
public BackgroundJob(I requestInfo, Callback<R> 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<R> {
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);
});
}
}
/*
* Copyright 2018 Erik Duisters <e.duisters1@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<BackgroundJob, Future<?>> jobMap = new HashMap<>();
private final Object jobMapLock = new Object();
private class MyThreadPoolExecutor extends ThreadPoolExecutor {
MyThreadPoolExecutor(int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> 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<Runnable> 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<BackgroundJob, Future<?>> 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<>());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment