File indexing completed on 2024-12-22 04:41:41

0001 /*
0002  * SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 package org.kde.kdeconnect.Plugins.SharePlugin;
0008 
0009 import android.app.DownloadManager;
0010 import android.content.ActivityNotFoundException;
0011 import android.content.Intent;
0012 import android.net.Uri;
0013 import android.os.Build;
0014 import android.util.Log;
0015 
0016 import androidx.annotation.GuardedBy;
0017 import androidx.core.content.ContextCompat;
0018 import androidx.core.content.FileProvider;
0019 import androidx.documentfile.provider.DocumentFile;
0020 
0021 import org.apache.commons.io.FilenameUtils;
0022 import org.apache.commons.io.IOUtils;
0023 import org.kde.kdeconnect.Device;
0024 import org.kde.kdeconnect.Helpers.FilesHelper;
0025 import org.kde.kdeconnect.Helpers.MediaStoreHelper;
0026 import org.kde.kdeconnect.NetworkPacket;
0027 import org.kde.kdeconnect.async.BackgroundJob;
0028 import org.kde.kdeconnect_tp.R;
0029 
0030 import java.io.BufferedOutputStream;
0031 import java.io.File;
0032 import java.io.IOException;
0033 import java.io.InputStream;
0034 import java.io.OutputStream;
0035 import java.nio.file.Files;
0036 import java.nio.file.Paths;
0037 import java.nio.file.attribute.FileTime;
0038 import java.util.ArrayList;
0039 import java.util.List;
0040 
0041 /**
0042  * A type of {@link BackgroundJob} that reads Files from another device.
0043  *
0044  * <p>
0045  *     We receive the requests as {@link NetworkPacket}s.
0046  * </p>
0047  * <p>
0048  *     Each packet should have a 'filename' property and a payload. If the payload is missing,
0049  *     we'll just create an empty file. You can add new packets anytime via
0050  *     {@link #addNetworkPacket(NetworkPacket)}.
0051  * </p>
0052  * <p>
0053  *     The I/O-part of this file reading is handled by {@link #receiveFile(InputStream, OutputStream)}.
0054  * </p>
0055  *
0056  * @see CompositeUploadFileJob
0057  */
0058 public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> {
0059     private final ReceiveNotification receiveNotification;
0060     private NetworkPacket currentNetworkPacket;
0061     private String currentFileName;
0062     private int currentFileNum;
0063     private long totalReceived;
0064     private long lastProgressTimeMillis;
0065     private long prevProgressPercentage;
0066 
0067     private final Object lock;                              //Use to protect concurrent access to the variables below
0068     @GuardedBy("lock")
0069     private final List<NetworkPacket> networkPacketList;
0070     @GuardedBy("lock")
0071     private int totalNumFiles;
0072     @GuardedBy("lock")
0073     private long totalPayloadSize;
0074     private boolean isRunning;
0075 
0076     CompositeReceiveFileJob(Device device, BackgroundJob.Callback<Void> callBack) {
0077         super(device, callBack);
0078 
0079         lock = new Object();
0080         networkPacketList = new ArrayList<>();
0081         receiveNotification = new ReceiveNotification(device, getId());
0082         currentFileNum = 0;
0083         totalNumFiles = 0;
0084         totalPayloadSize = 0;
0085         totalReceived = 0;
0086         lastProgressTimeMillis = 0;
0087         prevProgressPercentage = 0;
0088     }
0089 
0090     private Device getDevice() {
0091         return requestInfo;
0092     }
0093 
0094     boolean isRunning() { return isRunning; }
0095 
0096     void updateTotals(int numberOfFiles, long totalPayloadSize) {
0097         synchronized (lock) {
0098             this.totalNumFiles = numberOfFiles;
0099             this.totalPayloadSize = totalPayloadSize;
0100 
0101             receiveNotification.setTitle(getDevice().getContext().getResources()
0102                     .getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName()));
0103         }
0104     }
0105 
0106     void addNetworkPacket(NetworkPacket networkPacket) {
0107         synchronized (lock) {
0108             if (!networkPacketList.contains(networkPacket)) {
0109                 networkPacketList.add(networkPacket);
0110 
0111                 totalNumFiles = networkPacket.getInt(SharePlugin.KEY_NUMBER_OF_FILES, 1);
0112                 totalPayloadSize = networkPacket.getLong(SharePlugin.KEY_TOTAL_PAYLOAD_SIZE);
0113 
0114                 receiveNotification.setTitle(getDevice().getContext().getResources()
0115                         .getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName()));
0116             }
0117         }
0118     }
0119 
0120     @Override
0121     public void run() {
0122         boolean done;
0123         OutputStream outputStream = null;
0124 
0125         synchronized (lock) {
0126             done = networkPacketList.isEmpty();
0127         }
0128 
0129         try {
0130             DocumentFile fileDocument = null;
0131 
0132             isRunning = true;
0133 
0134             while (!done && !canceled) {
0135                 synchronized (lock) {
0136                     currentNetworkPacket = networkPacketList.get(0);
0137                 }
0138                 currentFileName = currentNetworkPacket.getString("filename", Long.toString(System.currentTimeMillis()));
0139                 currentFileNum++;
0140 
0141                 setProgress((int)prevProgressPercentage);
0142 
0143                 fileDocument = getDocumentFileFor(currentFileName, currentNetworkPacket.getBoolean("open", false));
0144 
0145                 if (currentNetworkPacket.hasPayload()) {
0146                     outputStream = new BufferedOutputStream(getDevice().getContext().getContentResolver().openOutputStream(fileDocument.getUri()));
0147                     InputStream inputStream = currentNetworkPacket.getPayload().getInputStream();
0148 
0149                     long received = receiveFile(inputStream, outputStream);
0150 
0151                     currentNetworkPacket.getPayload().close();
0152 
0153                     if ( received != currentNetworkPacket.getPayloadSize()) {
0154                         fileDocument.delete();
0155 
0156                         if (!canceled) {
0157                             throw new RuntimeException("Failed to receive: " + currentFileName + " received:" + received + " bytes, expected: " + currentNetworkPacket.getPayloadSize() + " bytes");
0158                         }
0159                     } else {
0160                         publishFile(fileDocument, received);
0161                     }
0162                 } else {
0163                     //TODO: Only set progress to 100 if this is the only file/packet to send
0164                     setProgress(100);
0165                     publishFile(fileDocument, 0);
0166                 }
0167 
0168                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
0169                     if (currentNetworkPacket.has("lastModified")) {
0170                         try {
0171                             long lastModified = currentNetworkPacket.getLong("lastModified");
0172                             Files.setLastModifiedTime(Paths.get(fileDocument.getUri().getPath()), FileTime.fromMillis(lastModified));
0173                         } catch (Exception e) {
0174                             Log.e("SharePlugin", "Can't set date on file");
0175                             e.printStackTrace();
0176                         }
0177                     }
0178                 }
0179 
0180                 boolean listIsEmpty;
0181 
0182                 synchronized (lock) {
0183                     networkPacketList.remove(0);
0184                     listIsEmpty = networkPacketList.isEmpty();
0185                 }
0186 
0187                 if (listIsEmpty && !canceled) {
0188                     try {
0189                         Thread.sleep(1000);
0190                     } catch (InterruptedException ignored) {}
0191 
0192                     synchronized (lock) {
0193                         if (currentFileNum < totalNumFiles && networkPacketList.isEmpty()) {
0194                             throw new RuntimeException("Failed to receive " + (totalNumFiles - currentFileNum + 1) + " files");
0195                         }
0196                     }
0197                 }
0198 
0199                 synchronized (lock) {
0200                     done = networkPacketList.isEmpty();
0201                 }
0202             }
0203 
0204             isRunning = false;
0205 
0206             if (canceled) {
0207                 receiveNotification.cancel();
0208                 return;
0209             }
0210 
0211             int numFiles;
0212             synchronized (lock) {
0213                 numFiles = totalNumFiles;
0214             }
0215 
0216             if (numFiles == 1 && currentNetworkPacket.getBoolean("open", false)) {
0217                 receiveNotification.cancel();
0218                 openFile(fileDocument);
0219             } else {
0220                 //Update the notification and allow to open the file from it
0221                 receiveNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_title, numFiles, getDevice().getName(), numFiles));
0222 
0223                 if (totalNumFiles == 1 && fileDocument != null) {
0224                     receiveNotification.setURI(fileDocument.getUri(), fileDocument.getType(), fileDocument.getName());
0225                 }
0226 
0227                 receiveNotification.show();
0228             }
0229             reportResult(null);
0230 
0231         } catch (ActivityNotFoundException e) {
0232             receiveNotification.setFinished(getDevice().getContext().getString(R.string.no_app_for_opening));
0233             receiveNotification.show();
0234         } catch (Exception e) {
0235             isRunning = false;
0236 
0237             Log.e("Shareplugin", "Error receiving file", e);
0238 
0239             int failedFiles;
0240             synchronized (lock) {
0241                 failedFiles = (totalNumFiles - currentFileNum + 1);
0242             }
0243 
0244             receiveNotification.setFailed(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, getDevice().getName(), failedFiles, totalNumFiles));
0245             receiveNotification.show();
0246             reportError(e);
0247         } finally {
0248             closeAllInputStreams();
0249             networkPacketList.clear();
0250             try {
0251                 IOUtils.close(outputStream);
0252             } catch (IOException ignored) {
0253             }
0254         }
0255     }
0256 
0257     private DocumentFile getDocumentFileFor(final String filename, final boolean open) throws RuntimeException {
0258         final DocumentFile destinationFolderDocument;
0259 
0260         String filenameToUse = filename;
0261 
0262         //We need to check for already existing files only when storing in the default path.
0263         //User-defined paths use the new Storage Access Framework that already handles this.
0264         //If the file should be opened immediately store it in the standard location to avoid the FileProvider trouble (See ReceiveNotification::setURI)
0265         if (open || !ShareSettingsFragment.isCustomDestinationEnabled(getDevice().getContext())) {
0266             final String defaultPath = ShareSettingsFragment.getDefaultDestinationDirectory().getAbsolutePath();
0267             filenameToUse = FilesHelper.findNonExistingNameForNewFile(defaultPath, filenameToUse);
0268             destinationFolderDocument = DocumentFile.fromFile(new File(defaultPath));
0269         } else {
0270             destinationFolderDocument = ShareSettingsFragment.getDestinationDirectory(getDevice().getContext());
0271         }
0272         String displayName = FilenameUtils.getBaseName(filenameToUse);
0273         String mimeType = FilesHelper.getMimeTypeFromFile(filenameToUse);
0274 
0275         if ("*/*".equals(mimeType)) {
0276             displayName = filenameToUse;
0277         }
0278 
0279         DocumentFile fileDocument = destinationFolderDocument.createFile(mimeType, displayName);
0280 
0281         if (fileDocument == null) {
0282             throw new RuntimeException(getDevice().getContext().getString(R.string.cannot_create_file, filenameToUse));
0283         }
0284 
0285         return fileDocument;
0286     }
0287 
0288     private long receiveFile(InputStream input, OutputStream output) throws IOException {
0289         byte[] data = new byte[4096];
0290         int count;
0291         long received = 0;
0292 
0293         while ((count = input.read(data)) >= 0 && !canceled) {
0294             received += count;
0295             totalReceived += count;
0296 
0297             output.write(data, 0, count);
0298 
0299             long progressPercentage;
0300             synchronized (lock) {
0301                 progressPercentage = (totalReceived * 100 / totalPayloadSize);
0302             }
0303             long curTimeMillis = System.currentTimeMillis();
0304 
0305             if (progressPercentage != prevProgressPercentage &&
0306                     (progressPercentage == 100 || curTimeMillis - lastProgressTimeMillis >= 500)) {
0307                 prevProgressPercentage = progressPercentage;
0308                 lastProgressTimeMillis = curTimeMillis;
0309                 setProgress((int)progressPercentage);
0310             }
0311         }
0312 
0313         output.flush();
0314 
0315         return received;
0316     }
0317 
0318     private void closeAllInputStreams() {
0319         for (NetworkPacket np : networkPacketList) {
0320             np.getPayload().close();
0321         }
0322     }
0323 
0324     private void setProgress(int progress) {
0325         synchronized (lock) {
0326             receiveNotification.setProgress(progress, getDevice().getContext().getResources()
0327                     .getQuantityString(R.plurals.incoming_files_text, totalNumFiles, currentFileName, currentFileNum, totalNumFiles));
0328         }
0329         receiveNotification.show();
0330     }
0331 
0332     private void publishFile(DocumentFile fileDocument, long size) {
0333         if (!ShareSettingsFragment.isCustomDestinationEnabled(getDevice().getContext())) {
0334             Log.i("SharePlugin", "Adding to downloads");
0335             DownloadManager manager = ContextCompat.getSystemService(getDevice().getContext(),
0336                     DownloadManager.class);
0337             manager.addCompletedDownload(fileDocument.getUri().getLastPathSegment(), getDevice().getName(), true, fileDocument.getType(), fileDocument.getUri().getPath(), size, false);
0338         } else {
0339             //Make sure it is added to the Android Gallery anyway
0340             Log.i("SharePlugin", "Adding to gallery");
0341             MediaStoreHelper.indexFile(getDevice().getContext(), fileDocument.getUri());
0342         }
0343     }
0344 
0345     private void openFile(DocumentFile fileDocument) {
0346         String mimeType = FilesHelper.getMimeTypeFromFile(fileDocument.getName());
0347         Intent intent = new Intent(Intent.ACTION_VIEW);
0348         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
0349             //Nougat and later require "content://" uris instead of "file://" uris
0350             File file = new File(fileDocument.getUri().getPath());
0351             Uri contentUri = FileProvider.getUriForFile(getDevice().getContext(), "org.kde.kdeconnect_tp.fileprovider", file);
0352             intent.setDataAndType(contentUri, mimeType);
0353             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
0354         } else {
0355             intent.setDataAndType(fileDocument.getUri(), mimeType);
0356         }
0357 
0358         // Open files for KDE Itinerary explicitly because Android's activity resolution sucks
0359         if (fileDocument.getName().endsWith(".itinerary")) {
0360             intent.setClassName("org.kde.itinerary", "org.kde.itinerary.Activity");
0361         }
0362 
0363         getDevice().getContext().startActivity(intent);
0364     }
0365 }