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 }