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