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 mXmppConnectionService.updateConversationUi();
129 if (acceptedAutomatically) {
130 mXmppConnectionService.getNotificationService().push(message);
131 }
132 }
133
134 private void changeStatus(int status) {
135 this.mStatus = status;
136 mXmppConnectionService.updateConversationUi();
137 }
138
139 private void showToastForException(Exception e) {
140 e.printStackTrace();
141 if (e instanceof java.net.UnknownHostException) {
142 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
143 } else if (e instanceof java.net.ConnectException) {
144 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
145 } else {
146 mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
147 }
148 }
149
150 private class FileSizeChecker implements Runnable {
151
152 private boolean interactive = false;
153
154 public FileSizeChecker(boolean interactive) {
155 this.interactive = interactive;
156 }
157
158 @Override
159 public void run() {
160 long size;
161 try {
162 size = retrieveFileSize();
163 } catch (SSLHandshakeException e) {
164 changeStatus(STATUS_OFFER_CHECK_FILESIZE);
165 HttpDownloadConnection.this.acceptedAutomatically = false;
166 HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
167 return;
168 } catch (IOException e) {
169 Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
170 if (interactive) {
171 showToastForException(e);
172 }
173 cancel();
174 return;
175 }
176 file.setExpectedSize(size);
177 if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
178 HttpDownloadConnection.this.acceptedAutomatically = true;
179 new Thread(new FileDownloader(interactive)).start();
180 } else {
181 changeStatus(STATUS_OFFER);
182 HttpDownloadConnection.this.acceptedAutomatically = false;
183 HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
184 }
185 }
186
187 private long retrieveFileSize() throws IOException {
188 try {
189 Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
190 changeStatus(STATUS_CHECKING);
191 HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
192 connection.setRequestMethod("HEAD");
193 connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName());
194 if (connection instanceof HttpsURLConnection) {
195 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
196 }
197 connection.connect();
198 String contentLength = connection.getHeaderField("Content-Length");
199 connection.disconnect();
200 if (contentLength == null) {
201 throw new IOException();
202 }
203 return Long.parseLong(contentLength, 10);
204 } catch (IOException e) {
205 throw e;
206 } catch (NumberFormatException e) {
207 throw new IOException();
208 }
209 }
210
211 }
212
213 private class FileDownloader implements Runnable {
214
215 private boolean interactive = false;
216
217 private OutputStream os;
218
219 public FileDownloader(boolean interactive) {
220 this.interactive = interactive;
221 }
222
223 @Override
224 public void run() {
225 try {
226 changeStatus(STATUS_DOWNLOADING);
227 download();
228 updateImageBounds();
229 finish();
230 } catch (SSLHandshakeException e) {
231 changeStatus(STATUS_OFFER);
232 } catch (IOException e) {
233 if (interactive) {
234 showToastForException(e);
235 }
236 cancel();
237 }
238 }
239
240 private void download() throws IOException {
241 InputStream is = null;
242 PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
243 try {
244 wakeLock.acquire();
245 HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
246 if (connection instanceof HttpsURLConnection) {
247 mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
248 }
249 connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
250 connection.connect();
251 is = new BufferedInputStream(connection.getInputStream());
252 file.getParentFile().mkdirs();
253 file.createNewFile();
254 os = AbstractConnectionManager.createOutputStream(file, true);
255 long transmitted = 0;
256 long expected = file.getExpectedSize();
257 int count = -1;
258 byte[] buffer = new byte[1024];
259 while ((count = is.read(buffer)) != -1) {
260 transmitted += count;
261 os.write(buffer, 0, count);
262 updateProgress((int) ((((double) transmitted) / expected) * 100));
263 }
264 os.flush();
265 } catch (IOException e) {
266 throw e;
267 } finally {
268 FileBackend.close(os);
269 FileBackend.close(is);
270 wakeLock.release();
271 }
272 }
273
274 private void updateImageBounds() {
275 message.setType(Message.TYPE_FILE);
276 mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl);
277 mXmppConnectionService.updateMessage(message);
278 }
279
280 }
281
282 public void updateProgress(int i) {
283 this.mProgress = i;
284 mXmppConnectionService.updateConversationUi();
285 }
286
287 @Override
288 public int getStatus() {
289 return this.mStatus;
290 }
291
292 @Override
293 public long getFileSize() {
294 if (this.file != null) {
295 return this.file.getExpectedSize();
296 } else {
297 return 0;
298 }
299 }
300
301 @Override
302 public int getProgress() {
303 return this.mProgress;
304 }
305}