1package eu.siacs.conversations.http;
2
3import android.content.Intent;
4import android.net.Uri;
5import android.os.PowerManager;
6import android.util.Log;
7
8import java.io.BufferedInputStream;
9import java.io.IOException;
10import java.io.InputStream;
11import java.io.OutputStream;
12import java.net.HttpURLConnection;
13import java.net.MalformedURLException;
14import java.net.URL;
15import java.util.Arrays;
16
17import javax.net.ssl.HttpsURLConnection;
18import javax.net.ssl.SSLHandshakeException;
19
20import eu.siacs.conversations.Config;
21import eu.siacs.conversations.R;
22import eu.siacs.conversations.entities.DownloadableFile;
23import eu.siacs.conversations.entities.Message;
24import eu.siacs.conversations.entities.Transferable;
25import eu.siacs.conversations.entities.TransferablePlaceholder;
26import eu.siacs.conversations.persistance.FileBackend;
27import eu.siacs.conversations.services.AbstractConnectionManager;
28import eu.siacs.conversations.services.XmppConnectionService;
29import eu.siacs.conversations.utils.CryptoHelper;
30
31public class HttpDownloadConnection implements Transferable {
32
33 private HttpConnectionManager mHttpConnectionManager;
34 private XmppConnectionService mXmppConnectionService;
35
36 private URL mUrl;
37 private Message message;
38 private DownloadableFile file;
39 private int mStatus = Transferable.STATUS_UNKNOWN;
40 private boolean acceptedAutomatically = false;
41 private int mProgress = 0;
42
43 public HttpDownloadConnection(HttpConnectionManager manager) {
44 this.mHttpConnectionManager = manager;
45 this.mXmppConnectionService = manager.getXmppConnectionService();
46 }
47
48 @Override
49 public boolean start() {
50 if (mXmppConnectionService.hasInternetConnection()) {
51 if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
52 checkFileSize(true);
53 } else {
54 new Thread(new FileDownloader(true)).start();
55 }
56 return true;
57 } else {
58 return false;
59 }
60 }
61
62 public void init(Message message) {
63 init(message, false);
64 }
65
66 public void init(Message message, boolean interactive) {
67 this.message = message;
68 this.message.setTransferable(this);
69 try {
70 if (message.hasFileOnRemoteHost()) {
71 mUrl = message.getFileParams().url;
72 } else {
73 mUrl = new URL(message.getBody());
74 }
75 String[] parts = mUrl.getPath().toLowerCase().split("\\.");
76 String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null;
77 String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
78 if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
79 this.message.setEncryption(Message.ENCRYPTION_PGP);
80 } else if (message.getEncryption() != Message.ENCRYPTION_OTR
81 && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
82 this.message.setEncryption(Message.ENCRYPTION_NONE);
83 }
84 String extension;
85 if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) {
86 extension = secondToLast;
87 } else {
88 extension = lastPart;
89 }
90 message.setRelativeFilePath(message.getUuid()+"."+extension);
91 this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
92 String reference = mUrl.getRef();
93 if (reference != null && reference.length() == 96) {
94 this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
95 }
96
97 if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
98 || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
99 && this.file.getKey() == null) {
100 this.message.setEncryption(Message.ENCRYPTION_NONE);
101 }
102 checkFileSize(interactive);
103 } catch (MalformedURLException e) {
104 this.cancel();
105 }
106 }
107
108 private void checkFileSize(boolean interactive) {
109 new Thread(new FileSizeChecker(interactive)).start();
110 }
111
112 public void cancel() {
113 mHttpConnectionManager.finishConnection(this);
114 if (message.isFileOrImage()) {
115 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
116 } else {
117 message.setTransferable(null);
118 }
119 mXmppConnectionService.updateConversationUi();
120 }
121
122 private void finish() {
123 Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
124 intent.setData(Uri.fromFile(file));
125 mXmppConnectionService.sendBroadcast(intent);
126 message.setTransferable(null);
127 mHttpConnectionManager.finishConnection(this);
128 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
129 message.getConversation().getAccount().getPgpDecryptionService().add(message);
130 }
131 mXmppConnectionService.updateConversationUi();
132 if (acceptedAutomatically) {
133 mXmppConnectionService.getNotificationService().push(message);
134 }
135 }
136
137 private void changeStatus(int status) {
138 this.mStatus = status;
139 mXmppConnectionService.updateConversationUi();
140 }
141
142 private void showToastForException(Exception e) {
143 e.printStackTrace();
144 if (e instanceof java.net.UnknownHostException) {
145 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
146 } else if (e instanceof java.net.ConnectException) {
147 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
148 } else {
149 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
150 }
151 }
152
153 private class FileSizeChecker implements Runnable {
154
155 private boolean interactive = false;
156
157 public FileSizeChecker(boolean interactive) {
158 this.interactive = interactive;
159 }
160
161 @Override
162 public void run() {
163 long size;
164 try {
165 size = retrieveFileSize();
166 } catch (SSLHandshakeException e) {
167 changeStatus(STATUS_OFFER_CHECK_FILESIZE);
168 HttpDownloadConnection.this.acceptedAutomatically = false;
169 HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
170 return;
171 } catch (IOException e) {
172 Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
173 if (interactive) {
174 showToastForException(e);
175 }
176 cancel();
177 return;
178 }
179 file.setExpectedSize(size);
180 if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
181 HttpDownloadConnection.this.acceptedAutomatically = true;
182 new Thread(new FileDownloader(interactive)).start();
183 } else {
184 changeStatus(STATUS_OFFER);
185 HttpDownloadConnection.this.acceptedAutomatically = false;
186 HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
187 }
188 }
189
190 private long retrieveFileSize() throws IOException {
191 try {
192 Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
193 changeStatus(STATUS_CHECKING);
194 HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
195 connection.setRequestMethod("HEAD");
196 connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName());
197 if (connection instanceof HttpsURLConnection) {
198 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
199 }
200 connection.connect();
201 String contentLength = connection.getHeaderField("Content-Length");
202 connection.disconnect();
203 if (contentLength == null) {
204 throw new IOException();
205 }
206 return Long.parseLong(contentLength, 10);
207 } catch (IOException e) {
208 throw e;
209 } catch (NumberFormatException e) {
210 throw new IOException();
211 }
212 }
213
214 }
215
216 private class FileDownloader implements Runnable {
217
218 private boolean interactive = false;
219
220 private OutputStream os;
221
222 public FileDownloader(boolean interactive) {
223 this.interactive = interactive;
224 }
225
226 @Override
227 public void run() {
228 try {
229 changeStatus(STATUS_DOWNLOADING);
230 download();
231 updateImageBounds();
232 finish();
233 } catch (SSLHandshakeException e) {
234 changeStatus(STATUS_OFFER);
235 } catch (IOException e) {
236 if (interactive) {
237 showToastForException(e);
238 }
239 cancel();
240 }
241 }
242
243 private void download() throws IOException {
244 InputStream is = null;
245 PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
246 try {
247 wakeLock.acquire();
248 HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
249 if (connection instanceof HttpsURLConnection) {
250 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
251 }
252 connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
253 connection.connect();
254 is = new BufferedInputStream(connection.getInputStream());
255 file.getParentFile().mkdirs();
256 file.createNewFile();
257 os = AbstractConnectionManager.createOutputStream(file, true);
258 long transmitted = 0;
259 long expected = file.getExpectedSize();
260 int count = -1;
261 byte[] buffer = new byte[1024];
262 while ((count = is.read(buffer)) != -1) {
263 transmitted += count;
264 os.write(buffer, 0, count);
265 updateProgress((int) ((((double) transmitted) / expected) * 100));
266 }
267 os.flush();
268 } catch (IOException e) {
269 throw e;
270 } finally {
271 FileBackend.close(os);
272 FileBackend.close(is);
273 wakeLock.release();
274 }
275 }
276
277 private void updateImageBounds() {
278 message.setType(Message.TYPE_FILE);
279 mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl);
280 mXmppConnectionService.updateMessage(message);
281 }
282
283 }
284
285 public void updateProgress(int i) {
286 this.mProgress = i;
287 mXmppConnectionService.updateConversationUi();
288 }
289
290 @Override
291 public int getStatus() {
292 return this.mStatus;
293 }
294
295 @Override
296 public long getFileSize() {
297 if (this.file != null) {
298 return this.file.getExpectedSize();
299 } else {
300 return 0;
301 }
302 }
303
304 @Override
305 public int getProgress() {
306 return this.mProgress;
307 }
308}