Differential D16491 Diff 47681 src/org/kde/kdeconnect/Plugins/SharePlugin/CompositeReceiveFileJob.java
Changeset View
Changeset View
Standalone View
Standalone View
src/org/kde/kdeconnect/Plugins/SharePlugin/CompositeReceiveFileJob.java
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | * Copyright 2018 Erik Duisters <e.duisters1@gmail.com> | ||||
3 | * | ||||
4 | * This program is free software; you can redistribute it and/or | ||||
5 | * modify it under the terms of the GNU General Public License as | ||||
6 | * published by the Free Software Foundation; either version 2 of | ||||
7 | * the License or (at your option) version 3 or any later version | ||||
8 | * accepted by the membership of KDE e.V. (or its successor approved | ||||
9 | * by the membership of KDE e.V.), which shall act as a proxy | ||||
10 | * defined in Section 14 of version 3 of the license. | ||||
11 | * | ||||
12 | * This program is distributed in the hope that it will be useful, | ||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
15 | * GNU General Public License for more details. | ||||
16 | * | ||||
17 | * You should have received a copy of the GNU General Public License | ||||
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
19 | */ | ||||
20 | | ||||
21 | package org.kde.kdeconnect.Plugins.SharePlugin; | ||||
22 | | ||||
23 | import android.app.DownloadManager; | ||||
24 | import android.content.Context; | ||||
25 | import android.content.Intent; | ||||
26 | import android.net.Uri; | ||||
27 | import android.os.Build; | ||||
28 | import android.support.v4.content.FileProvider; | ||||
29 | import android.support.v4.provider.DocumentFile; | ||||
30 | import android.util.Log; | ||||
31 | | ||||
32 | import org.kde.kdeconnect.Device; | ||||
33 | import org.kde.kdeconnect.Helpers.FilesHelper; | ||||
34 | import org.kde.kdeconnect.Helpers.MediaStoreHelper; | ||||
35 | import org.kde.kdeconnect.NetworkPacket; | ||||
36 | import org.kde.kdeconnect.async.BackgroundJob; | ||||
37 | import org.kde.kdeconnect_tp.R; | ||||
38 | | ||||
39 | import java.io.BufferedOutputStream; | ||||
40 | import java.io.File; | ||||
41 | import java.io.IOException; | ||||
42 | import java.io.InputStream; | ||||
43 | import java.io.OutputStream; | ||||
44 | import java.util.ArrayList; | ||||
45 | import java.util.List; | ||||
46 | | ||||
47 | public class CompositeReceiveFileJob extends BackgroundJob<Device, Void> { | ||||
48 | private final ShareNotification shareNotification; | ||||
49 | private NetworkPacket currentNetworkPacket; | ||||
50 | private String currentFileName; | ||||
51 | private int currentFileNum; | ||||
52 | private long totalReceived; | ||||
53 | private long lastProgressTimeMillis; | ||||
54 | private long prevProgressPercentage; | ||||
55 | | ||||
56 | private final Object lock; //Use to protect concurrent access to the variables below | ||||
57 | private final List<NetworkPacket> networkPacketList; | ||||
58 | private int totalNumFiles; | ||||
59 | private long totalPayloadSize; | ||||
60 | | ||||
61 | CompositeReceiveFileJob(Device device, BackgroundJob.Callback<Void> callBack) { | ||||
62 | super(device, callBack); | ||||
63 | | ||||
64 | lock = new Object(); | ||||
65 | networkPacketList = new ArrayList<>(); | ||||
66 | shareNotification = new ShareNotification(device); | ||||
67 | shareNotification.setJobId(getId()); | ||||
68 | currentFileNum = 0; | ||||
69 | totalNumFiles = 0; | ||||
70 | totalPayloadSize = 0; | ||||
71 | totalReceived = 0; | ||||
72 | lastProgressTimeMillis = 0; | ||||
73 | prevProgressPercentage = 0; | ||||
74 | } | ||||
75 | | ||||
76 | private Device getDevice() { | ||||
77 | return requestInfo; | ||||
78 | } | ||||
79 | | ||||
80 | void addNetworkPacket(NetworkPacket networkPacket) { | ||||
81 | if (!networkPacketList.contains(networkPacket)) { | ||||
82 | synchronized (lock) { | ||||
83 | networkPacketList.add(networkPacket); | ||||
84 | | ||||
85 | totalNumFiles = networkPacket.getInt(SharePlugin.KEY_NUMBER_OF_FILES, 1); | ||||
86 | totalPayloadSize = networkPacket.getLong(SharePlugin.KEY_TOTAL_PAYLOAD_SIZE); | ||||
87 | | ||||
88 | shareNotification.setTitle(getDevice().getContext().getResources() | ||||
89 | .getQuantityString(R.plurals.incoming_file_title, totalNumFiles, totalNumFiles, getDevice().getName())); | ||||
90 | } | ||||
91 | } | ||||
92 | } | ||||
93 | | ||||
94 | @Override | ||||
95 | public void run() { | ||||
96 | boolean done; | ||||
97 | OutputStream outputStream = null; | ||||
98 | | ||||
99 | synchronized (lock) { | ||||
100 | done = networkPacketList.isEmpty(); | ||||
101 | } | ||||
102 | | ||||
103 | try { | ||||
104 | DocumentFile fileDocument = null; | ||||
105 | | ||||
106 | while (!done && !canceled) { | ||||
107 | synchronized (lock) { | ||||
108 | currentNetworkPacket = networkPacketList.get(0); | ||||
109 | } | ||||
110 | currentFileName = currentNetworkPacket.getString("filename", Long.toString(System.currentTimeMillis())); | ||||
111 | currentFileNum++; | ||||
112 | | ||||
113 | setProgress((int)prevProgressPercentage); | ||||
114 | | ||||
115 | fileDocument = getDocumentFileFor(currentFileName, currentNetworkPacket.getBoolean("open")); | ||||
116 | | ||||
117 | if (currentNetworkPacket.hasPayload()) { | ||||
118 | outputStream = new BufferedOutputStream(getDevice().getContext().getContentResolver().openOutputStream(fileDocument.getUri())); | ||||
119 | InputStream inputStream = currentNetworkPacket.getPayload().getInputStream(); | ||||
120 | | ||||
121 | long received = receiveFile(inputStream, outputStream); | ||||
122 | | ||||
123 | currentNetworkPacket.getPayload().close(); | ||||
124 | | ||||
125 | if ( received != currentNetworkPacket.getPayloadSize()) { | ||||
126 | fileDocument.delete(); | ||||
127 | | ||||
128 | if (!canceled) { | ||||
129 | throw new RuntimeException("Failed to receive: " + currentFileName + " received:" + received + " bytes, expected: " + currentNetworkPacket.getPayloadSize() + " bytes"); | ||||
130 | } | ||||
131 | } else { | ||||
132 | publishFile(fileDocument, received); | ||||
133 | } | ||||
134 | } else { | ||||
135 | setProgress(100); | ||||
136 | publishFile(fileDocument, 0); | ||||
137 | } | ||||
138 | | ||||
139 | boolean listIsEmpty; | ||||
140 | | ||||
141 | synchronized (lock) { | ||||
142 | networkPacketList.remove(0); | ||||
143 | listIsEmpty = networkPacketList.isEmpty(); | ||||
144 | } | ||||
145 | | ||||
146 | if (listIsEmpty && !canceled) { | ||||
147 | try { | ||||
148 | Thread.sleep(250); | ||||
149 | } catch (InterruptedException ignored) {} | ||||
150 | | ||||
151 | synchronized (lock) { | ||||
152 | if (currentFileNum < totalNumFiles && networkPacketList.isEmpty()) { | ||||
153 | throw new RuntimeException("Failed to receive " + (totalNumFiles - currentFileNum + 1) + " files"); | ||||
154 | } | ||||
155 | } | ||||
156 | } | ||||
157 | | ||||
158 | synchronized (lock) { | ||||
159 | done = networkPacketList.isEmpty(); | ||||
160 | } | ||||
161 | } | ||||
162 | | ||||
163 | if (canceled) { | ||||
164 | Log.e("ERIK", "I've been cancelled"); | ||||
165 | shareNotification.cancel(); | ||||
166 | return; | ||||
167 | } | ||||
168 | | ||||
169 | int numFiles; | ||||
170 | synchronized (lock) { | ||||
171 | numFiles = totalNumFiles; | ||||
172 | } | ||||
173 | | ||||
174 | if (numFiles == 1 && currentNetworkPacket.has("open")) { | ||||
175 | shareNotification.cancel(); | ||||
176 | openFile(fileDocument); | ||||
177 | } else { | ||||
178 | //Update the notification and allow to open the file from it | ||||
179 | shareNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_title, numFiles, numFiles, getDevice().getName())); | ||||
180 | | ||||
181 | if (totalNumFiles == 1 && fileDocument != null) { | ||||
182 | shareNotification.setURI(fileDocument.getUri(), fileDocument.getType(), fileDocument.getName()); | ||||
183 | } | ||||
184 | | ||||
185 | shareNotification.show(); | ||||
186 | } | ||||
187 | reportResult(null); | ||||
188 | } catch (Exception e) { | ||||
189 | int failedFiles; | ||||
190 | synchronized (lock) { | ||||
191 | failedFiles = (totalNumFiles - currentFileNum + 1); | ||||
192 | } | ||||
193 | shareNotification.setFinished(getDevice().getContext().getResources().getQuantityString(R.plurals.received_files_fail_title, failedFiles, failedFiles, totalNumFiles, getDevice().getName())); | ||||
194 | shareNotification.show(); | ||||
195 | reportError(e); | ||||
196 | } finally { | ||||
197 | Log.e("ERIK", "Closing all input and output streams"); | ||||
198 | closeAllInputStreams(); | ||||
199 | networkPacketList.clear(); | ||||
200 | if (outputStream != null) { | ||||
201 | try { | ||||
202 | outputStream.close(); | ||||
203 | } catch (IOException ignored) {} | ||||
204 | } | ||||
205 | } | ||||
206 | } | ||||
207 | | ||||
208 | private DocumentFile getDocumentFileFor(final String filename, final boolean open) throws RuntimeException { | ||||
209 | final DocumentFile destinationFolderDocument; | ||||
210 | | ||||
211 | String filenameToUse = filename; | ||||
212 | | ||||
213 | //We need to check for already existing files only when storing in the default path. | ||||
214 | //User-defined paths use the new Storage Access Framework that already handles this. | ||||
215 | //If the file should be opened immediately store it in the standard location to avoid the FileProvider trouble (See ShareNotification::setURI) | ||||
216 | if (open || !ShareSettingsActivity.isCustomDestinationEnabled(getDevice().getContext())) { | ||||
217 | final String defaultPath = ShareSettingsActivity.getDefaultDestinationDirectory().getAbsolutePath(); | ||||
218 | filenameToUse = FilesHelper.findNonExistingNameForNewFile(defaultPath, filenameToUse); | ||||
219 | destinationFolderDocument = DocumentFile.fromFile(new File(defaultPath)); | ||||
220 | } else { | ||||
221 | destinationFolderDocument = ShareSettingsActivity.getDestinationDirectory(getDevice().getContext()); | ||||
222 | } | ||||
223 | String displayName = FilesHelper.getFileNameWithoutExt(filenameToUse); | ||||
224 | String mimeType = FilesHelper.getMimeTypeFromFile(filenameToUse); | ||||
225 | | ||||
226 | if ("*/*".equals(mimeType)) { | ||||
227 | displayName = filenameToUse; | ||||
228 | } | ||||
229 | | ||||
230 | DocumentFile fileDocument = destinationFolderDocument.createFile(mimeType, displayName); | ||||
231 | | ||||
232 | if (fileDocument == null) { | ||||
233 | throw new RuntimeException(getDevice().getContext().getString(R.string.cannot_create_file, filenameToUse)); | ||||
234 | } | ||||
235 | | ||||
236 | return fileDocument; | ||||
237 | } | ||||
238 | | ||||
239 | private long receiveFile(InputStream input, OutputStream output) throws IOException { | ||||
240 | byte data[] = new byte[4096]; | ||||
241 | int count; | ||||
242 | long received = 0; | ||||
243 | | ||||
244 | while ((count = input.read(data)) >= 0 && !canceled) { | ||||
245 | received += count; | ||||
246 | totalReceived += count; | ||||
247 | | ||||
248 | output.write(data, 0, count); | ||||
249 | | ||||
250 | long progressPercentage; | ||||
251 | synchronized (lock) { | ||||
252 | progressPercentage = (totalReceived * 100 / totalPayloadSize); | ||||
253 | } | ||||
254 | long curTimeMillis = System.currentTimeMillis(); | ||||
255 | | ||||
256 | if (progressPercentage != prevProgressPercentage && | ||||
257 | (progressPercentage == 100 || curTimeMillis - lastProgressTimeMillis >= 500)) { | ||||
258 | prevProgressPercentage = progressPercentage; | ||||
259 | lastProgressTimeMillis = curTimeMillis; | ||||
260 | setProgress((int)progressPercentage); | ||||
261 | } | ||||
262 | } | ||||
263 | | ||||
264 | output.flush(); | ||||
265 | | ||||
266 | return received; | ||||
267 | } | ||||
268 | | ||||
269 | private void closeAllInputStreams() { | ||||
270 | for (NetworkPacket np : networkPacketList) { | ||||
271 | Log.e("ERIK", "closing a networkPackets payload"); | ||||
272 | np.getPayload().close(); | ||||
273 | } | ||||
274 | } | ||||
275 | | ||||
276 | private void setProgress(int progress) { | ||||
277 | synchronized (lock) { | ||||
278 | shareNotification.setProgress(progress, getDevice().getContext().getResources() | ||||
279 | .getQuantityString(R.plurals.incoming_files_text, totalNumFiles, currentFileName, currentFileNum, totalNumFiles)); | ||||
280 | } | ||||
281 | shareNotification.show(); | ||||
282 | } | ||||
283 | | ||||
284 | private void publishFile(DocumentFile fileDocument, long size) { | ||||
285 | if (!ShareSettingsActivity.isCustomDestinationEnabled(getDevice().getContext())) { | ||||
286 | Log.i("SharePlugin", "Adding to downloads"); | ||||
287 | DownloadManager manager = (DownloadManager) getDevice().getContext().getSystemService(Context.DOWNLOAD_SERVICE); | ||||
288 | manager.addCompletedDownload(fileDocument.getUri().getLastPathSegment(), getDevice().getName(), true, fileDocument.getType(), fileDocument.getUri().getPath(), size, false); | ||||
289 | } else { | ||||
290 | //Make sure it is added to the Android Gallery anyway | ||||
291 | Log.i("SharePlugin", "Adding to gallery"); | ||||
292 | MediaStoreHelper.indexFile(getDevice().getContext(), fileDocument.getUri()); | ||||
293 | } | ||||
294 | } | ||||
295 | | ||||
296 | private void openFile(DocumentFile fileDocument) { | ||||
297 | Intent intent = new Intent(Intent.ACTION_VIEW); | ||||
298 | if (Build.VERSION.SDK_INT >= 24) { | ||||
299 | //Nougat and later require "content://" uris instead of "file://" uris | ||||
300 | File file = new File(fileDocument.getUri().getPath()); | ||||
301 | Uri contentUri = FileProvider.getUriForFile(getDevice().getContext(), "org.kde.kdeconnect_tp.fileprovider", file); | ||||
302 | intent.setDataAndType(contentUri, fileDocument.getType()); | ||||
303 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||
304 | } else { | ||||
305 | intent.setDataAndType(fileDocument.getUri(), fileDocument.getType()); | ||||
306 | } | ||||
307 | | ||||
308 | getDevice().getContext().startActivity(intent); | ||||
309 | } | ||||
310 | } |