2019-01-06 14:06:24 +01:00
|
|
|
/*
|
|
|
|
* 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.app.DownloadManager;
|
2019-03-24 16:43:04 +01:00
|
|
|
import android.content.ActivityNotFoundException;
|
2019-01-06 14:06:24 +01:00
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.net.Uri;
|
|
|
|
import android.os.Build;
|
|
|
|
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;
|
2019-03-08 19:07:01 +01:00
|
|
|
import org.kde.kdeconnect.async.BackgroundJob;
|
2019-01-06 14:06:24 +01:00
|
|
|
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;
|
|
|
|
|
2019-07-21 10:31:47 -04:00
|
|
|
import androidx.annotation.GuardedBy;
|
2019-01-06 14:06:24 +01:00
|
|
|
import androidx.core.content.FileProvider;
|
|
|
|
import androidx.documentfile.provider.DocumentFile;
|
|
|
|
|
2019-07-21 10:31:47 -04:00
|
|
|
/**
|
|
|
|
* A type of {@link BackgroundJob} that reads Files from a list of {@link NetworkPacket}s.
|
|
|
|
*
|
|
|
|
* <p>
|
|
|
|
* Each packet should have a 'filename' property and a payload. If the payload is missing,
|
|
|
|
* we'll just create an empty file. You can add new packets anytime before this job completes
|
|
|
|
* via {@link #addNetworkPacket(NetworkPacket)}.
|
|
|
|
* </p>
|
|
|
|
* <p>
|
|
|
|
* The I/O-part of this file reading is handled by {@link #receiveFile(InputStream, OutputStream)}.
|
|
|
|
* </p>
|
|
|
|
*
|
|
|
|
* @see CompositeUploadFileJob
|
|
|
|
*/
|
2019-03-08 19:07:01 +01:00
|
|
|
public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
|
2019-04-07 17:54:12 +00:00
|
|
|
private final ReceiveNotification receiveNotification;
|
2019-01-06 14:06:24 +01:00
|
|
|
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
|
2019-07-21 10:31:47 -04:00
|
|
|
@GuardedBy("lock")
|
2019-01-06 14:06:24 +01:00
|
|
|
private final List<NetworkPacket> networkPacketList;
|
2019-07-21 10:31:47 -04:00
|
|
|
@GuardedBy("lock")
|
2019-01-06 14:06:24 +01:00
|
|
|
private int totalNumFiles;
|
2019-07-21 10:31:47 -04:00
|
|
|
@GuardedBy("lock")
|
2019-01-06 14:06:24 +01:00
|
|
|
private long totalPayloadSize;
|
2019-01-26 16:59:39 +01:00
|
|
|
private boolean isRunning;
|
2019-01-06 14:06:24 +01:00
|
|
|
|
2019-03-08 19:07:01 +01:00
|
|
|
CompositeReceiveFileJob(Device device, BackgroundJob.Callback<Void> callBack) {
|
|
|
|
super(device, callBack);
|
2019-01-06 14:06:24 +01:00
|
|
|
|
|
|
|
lock = new Object();
|
|
|
|
networkPacketList = new ArrayList<>();
|
2019-06-04 12:51:24 +00:00
|
|
|
receiveNotification = new ReceiveNotification(device, getId());
|
2019-01-06 14:06:24 +01:00
|
|
|
currentFileNum = 0;
|
|
|
|
totalNumFiles = 0;
|
|
|
|
totalPayloadSize = 0;
|
|
|
|
totalReceived = 0;
|
|
|
|
lastProgressTimeMillis = 0;
|
|
|
|
prevProgressPercentage = 0;
|
2019-03-08 19:07:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private Device getDevice() {
|
|
|
|
return requestInfo;
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
|
2019-01-26 16:59:39 +01:00
|
|
|
boolean isRunning() { return isRunning; }
|
|
|
|
|
|
|
|
void updateTotals(int numberOfFiles, long totalPayloadSize) {
|
|
|
|
synchronized (lock) {
|
|
|
|
this.totalNumFiles = numberOfFiles;
|
|
|
|
this.totalPayloadSize = totalPayloadSize;
|
|
|
|
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.setTitle(getDevice().getContext().getResources()
|
2019-03-08 19:07:01 +01:00
|
|
|
.getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName()));
|
2019-01-26 16:59:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-06 14:06:24 +01:00
|
|
|
void addNetworkPacket(NetworkPacket networkPacket) {
|
2019-01-26 16:59:39 +01:00
|
|
|
synchronized (lock) {
|
|
|
|
if (!networkPacketList.contains(networkPacket)) {
|
2019-01-06 14:06:24 +01:00
|
|
|
networkPacketList.add(networkPacket);
|
|
|
|
|
|
|
|
totalNumFiles = networkPacket.getInt(SharePlugin.KEY_NUMBER_OF_FILES, 1);
|
|
|
|
totalPayloadSize = networkPacket.getLong(SharePlugin.KEY_TOTAL_PAYLOAD_SIZE);
|
|
|
|
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.setTitle(getDevice().getContext().getResources()
|
2019-03-08 19:07:01 +01:00
|
|
|
.getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName()));
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
boolean done;
|
|
|
|
OutputStream outputStream = null;
|
|
|
|
|
|
|
|
synchronized (lock) {
|
|
|
|
done = networkPacketList.isEmpty();
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
DocumentFile fileDocument = null;
|
|
|
|
|
2019-01-26 16:59:39 +01:00
|
|
|
isRunning = true;
|
|
|
|
|
2019-03-08 19:07:01 +01:00
|
|
|
while (!done && !canceled) {
|
2019-01-06 14:06:24 +01:00
|
|
|
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()) {
|
2019-03-08 19:07:01 +01:00
|
|
|
outputStream = new BufferedOutputStream(getDevice().getContext().getContentResolver().openOutputStream(fileDocument.getUri()));
|
2019-01-06 14:06:24 +01:00
|
|
|
InputStream inputStream = currentNetworkPacket.getPayload().getInputStream();
|
|
|
|
|
|
|
|
long received = receiveFile(inputStream, outputStream);
|
|
|
|
|
|
|
|
currentNetworkPacket.getPayload().close();
|
|
|
|
|
|
|
|
if ( received != currentNetworkPacket.getPayloadSize()) {
|
|
|
|
fileDocument.delete();
|
2019-03-08 19:07:01 +01:00
|
|
|
|
|
|
|
if (!canceled) {
|
|
|
|
throw new RuntimeException("Failed to receive: " + currentFileName + " received:" + received + " bytes, expected: " + currentNetworkPacket.getPayloadSize() + " bytes");
|
|
|
|
}
|
2019-01-06 14:06:24 +01:00
|
|
|
} else {
|
|
|
|
publishFile(fileDocument, received);
|
|
|
|
}
|
|
|
|
} else {
|
2019-04-07 17:54:12 +00:00
|
|
|
//TODO: Only set progress to 100 if this is the only file/packet to send
|
2019-01-06 14:06:24 +01:00
|
|
|
setProgress(100);
|
|
|
|
publishFile(fileDocument, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean listIsEmpty;
|
|
|
|
|
|
|
|
synchronized (lock) {
|
|
|
|
networkPacketList.remove(0);
|
|
|
|
listIsEmpty = networkPacketList.isEmpty();
|
|
|
|
}
|
|
|
|
|
2019-03-08 19:07:01 +01:00
|
|
|
if (listIsEmpty && !canceled) {
|
2019-01-06 14:06:24 +01:00
|
|
|
try {
|
2019-01-26 16:59:39 +01:00
|
|
|
Thread.sleep(1000);
|
2019-01-06 14:06:24 +01:00
|
|
|
} 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-26 16:59:39 +01:00
|
|
|
isRunning = false;
|
|
|
|
|
2019-03-08 19:07:01 +01:00
|
|
|
if (canceled) {
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.cancel();
|
2019-03-08 19:07:01 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-01-06 14:06:24 +01:00
|
|
|
int numFiles;
|
|
|
|
synchronized (lock) {
|
|
|
|
numFiles = totalNumFiles;
|
|
|
|
}
|
|
|
|
|
2019-06-14 15:47:16 +02:00
|
|
|
if (numFiles == 1 && currentNetworkPacket.getBoolean("open", false)) {
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.cancel();
|
2019-01-06 14:06:24 +01:00
|
|
|
openFile(fileDocument);
|
|
|
|
} else {
|
|
|
|
//Update the notification and allow to open the file from it
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_title, numFiles, getDevice().getName(), numFiles));
|
2019-01-06 14:06:24 +01:00
|
|
|
|
|
|
|
if (totalNumFiles == 1 && fileDocument != null) {
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.setURI(fileDocument.getUri(), fileDocument.getType(), fileDocument.getName());
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.show();
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
2019-03-08 19:07:01 +01:00
|
|
|
reportResult(null);
|
2019-03-24 16:43:04 +01:00
|
|
|
|
|
|
|
} catch (ActivityNotFoundException e) {
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.setFinished(getDevice().getContext().getString(R.string.no_app_for_opening));
|
|
|
|
receiveNotification.show();
|
2019-01-06 14:06:24 +01:00
|
|
|
} catch (Exception e) {
|
2019-01-26 16:59:39 +01:00
|
|
|
isRunning = false;
|
|
|
|
|
2019-03-24 15:13:00 +01:00
|
|
|
Log.e("Shareplugin", "Error receiving file", e);
|
|
|
|
|
2019-01-06 14:06:24 +01:00
|
|
|
int failedFiles;
|
|
|
|
synchronized (lock) {
|
|
|
|
failedFiles = (totalNumFiles - currentFileNum + 1);
|
|
|
|
}
|
2019-03-08 19:07:01 +01:00
|
|
|
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, getDevice().getName(), failedFiles, totalNumFiles));
|
|
|
|
receiveNotification.show();
|
2019-03-08 19:07:01 +01:00
|
|
|
reportError(e);
|
2019-01-06 14:06:24 +01:00
|
|
|
} 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.
|
2019-04-07 17:54:12 +00:00
|
|
|
//If the file should be opened immediately store it in the standard location to avoid the FileProvider trouble (See ReceiveNotification::setURI)
|
2019-03-08 19:07:01 +01:00
|
|
|
if (open || !ShareSettingsFragment.isCustomDestinationEnabled(getDevice().getContext())) {
|
2019-01-06 14:06:24 +01:00
|
|
|
final String defaultPath = ShareSettingsFragment.getDefaultDestinationDirectory().getAbsolutePath();
|
|
|
|
filenameToUse = FilesHelper.findNonExistingNameForNewFile(defaultPath, filenameToUse);
|
|
|
|
destinationFolderDocument = DocumentFile.fromFile(new File(defaultPath));
|
|
|
|
} else {
|
2019-03-08 19:07:01 +01:00
|
|
|
destinationFolderDocument = ShareSettingsFragment.getDestinationDirectory(getDevice().getContext());
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
String displayName = FilesHelper.getFileNameWithoutExt(filenameToUse);
|
|
|
|
String mimeType = FilesHelper.getMimeTypeFromFile(filenameToUse);
|
|
|
|
|
|
|
|
if ("*/*".equals(mimeType)) {
|
|
|
|
displayName = filenameToUse;
|
|
|
|
}
|
|
|
|
|
|
|
|
DocumentFile fileDocument = destinationFolderDocument.createFile(mimeType, displayName);
|
|
|
|
|
|
|
|
if (fileDocument == null) {
|
2019-03-08 19:07:01 +01:00
|
|
|
throw new RuntimeException(getDevice().getContext().getString(R.string.cannot_create_file, filenameToUse));
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return fileDocument;
|
|
|
|
}
|
|
|
|
|
|
|
|
private long receiveFile(InputStream input, OutputStream output) throws IOException {
|
2019-04-19 01:11:35 +02:00
|
|
|
byte[] data = new byte[4096];
|
2019-01-06 14:06:24 +01:00
|
|
|
int count;
|
|
|
|
long received = 0;
|
|
|
|
|
2019-03-08 19:07:01 +01:00
|
|
|
while ((count = input.read(data)) >= 0 && !canceled) {
|
2019-01-06 14:06:24 +01:00
|
|
|
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) {
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.setProgress(progress, getDevice().getContext().getResources()
|
2019-01-06 14:06:24 +01:00
|
|
|
.getQuantityString(R.plurals.incoming_files_text, totalNumFiles, currentFileName, currentFileNum, totalNumFiles));
|
|
|
|
}
|
2019-04-07 17:54:12 +00:00
|
|
|
receiveNotification.show();
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private void publishFile(DocumentFile fileDocument, long size) {
|
2019-03-08 19:07:01 +01:00
|
|
|
if (!ShareSettingsFragment.isCustomDestinationEnabled(getDevice().getContext())) {
|
2019-01-06 14:06:24 +01:00
|
|
|
Log.i("SharePlugin", "Adding to downloads");
|
2019-03-08 19:07:01 +01:00
|
|
|
DownloadManager manager = (DownloadManager) getDevice().getContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
|
|
|
manager.addCompletedDownload(fileDocument.getUri().getLastPathSegment(), getDevice().getName(), true, fileDocument.getType(), fileDocument.getUri().getPath(), size, false);
|
2019-01-06 14:06:24 +01:00
|
|
|
} else {
|
|
|
|
//Make sure it is added to the Android Gallery anyway
|
|
|
|
Log.i("SharePlugin", "Adding to gallery");
|
2019-03-08 19:07:01 +01:00
|
|
|
MediaStoreHelper.indexFile(getDevice().getContext(), fileDocument.getUri());
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void openFile(DocumentFile fileDocument) {
|
2019-03-05 00:28:24 +01:00
|
|
|
String mimeType = FilesHelper.getMimeTypeFromFile(fileDocument.getName());
|
2019-01-06 14:06:24 +01:00
|
|
|
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());
|
2019-03-08 19:07:01 +01:00
|
|
|
Uri contentUri = FileProvider.getUriForFile(getDevice().getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
|
2019-03-05 00:28:24 +01:00
|
|
|
intent.setDataAndType(contentUri, mimeType);
|
2019-03-24 15:13:00 +01:00
|
|
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
|
2019-01-06 14:06:24 +01:00
|
|
|
} else {
|
2019-03-05 00:28:24 +01:00
|
|
|
intent.setDataAndType(fileDocument.getUri(), mimeType);
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
|
2019-03-08 19:07:01 +01:00
|
|
|
getDevice().getContext().startActivity(intent);
|
2019-01-06 14:06:24 +01:00
|
|
|
}
|
|
|
|
}
|