]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/async/SendTransactionTask.java
b444e332e3b00580bb1f1b979bdbbb390d009cfe
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / async / SendTransactionTask.java
1 /*
2  * Copyright © 2019 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.async;
19
20 import android.os.AsyncTask;
21 import android.util.Log;
22
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.databind.ObjectWriter;
25
26 import net.ktnx.mobileledger.json.ParsedLedgerTransaction;
27 import net.ktnx.mobileledger.model.LedgerTransaction;
28 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
29 import net.ktnx.mobileledger.model.MobileLedgerProfile;
30 import net.ktnx.mobileledger.utils.Globals;
31 import net.ktnx.mobileledger.utils.Logger;
32 import net.ktnx.mobileledger.utils.NetworkUtil;
33 import net.ktnx.mobileledger.utils.UrlEncodedFormData;
34
35 import java.io.BufferedReader;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.InputStreamReader;
39 import java.io.OutputStream;
40 import java.net.HttpURLConnection;
41 import java.nio.charset.StandardCharsets;
42 import java.util.List;
43 import java.util.Locale;
44 import java.util.Map;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47
48 import static android.os.SystemClock.sleep;
49 import static net.ktnx.mobileledger.utils.Logger.debug;
50
51 public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void> {
52     private final TaskCallback taskCallback;
53     protected String error;
54     private String token;
55     private String session;
56     private LedgerTransaction ltr;
57     private MobileLedgerProfile mProfile;
58     private boolean simulate = false;
59
60     public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile,
61                                boolean simulate) {
62         taskCallback = callback;
63         mProfile = profile;
64         this.simulate = simulate;
65     }
66     public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile) {
67         taskCallback = callback;
68         mProfile = profile;
69         simulate = false;
70     }
71     private boolean sendOK() throws IOException {
72         if (simulate) {
73             try {
74                 Thread.sleep(1500);
75                 if (Math.random() > 0.3)
76                     throw new RuntimeException("Simulated test exception");
77             }
78             catch (InterruptedException ex) {
79                 Logger.debug("network", ex.toString());
80             }
81
82             return true;
83         }
84
85         HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
86         http.setRequestMethod("PUT");
87         http.setRequestProperty("Content-Type", "application/json");
88         http.setRequestProperty("Accept", "*/*");
89
90         ParsedLedgerTransaction jsonTransaction;
91         jsonTransaction = ltr.toParsedLedgerTransaction();
92         ObjectMapper mapper = new ObjectMapper();
93         ObjectWriter writer = mapper.writerFor(ParsedLedgerTransaction.class);
94         String body = writer.writeValueAsString(jsonTransaction);
95
96         byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
97         http.setDoOutput(true);
98         http.setDoInput(true);
99         http.addRequestProperty("Content-Length", String.valueOf(bodyBytes.length));
100
101         debug("network", "request header: " + http.getRequestProperties()
102                                                   .toString());
103
104         try (OutputStream req = http.getOutputStream()) {
105             debug("network", "Request body: " + body);
106             req.write(bodyBytes);
107
108             final int responseCode = http.getResponseCode();
109             debug("network",
110                     String.format("Response: %d %s", responseCode, http.getResponseMessage()));
111
112             try (InputStream resp = http.getErrorStream()) {
113
114                 switch (responseCode) {
115                     case 200:
116                     case 201:
117                         break;
118                     case 405:
119                         return false; // will cause a retry with the legacy method
120                     default:
121                         BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
122                         String line = reader.readLine();
123                         debug("network", "Response content: " + line);
124                         throw new IOException(
125                                 String.format("Error response code %d", responseCode));
126                 }
127             }
128         }
129
130         return true;
131     }
132     private boolean legacySendOK() throws IOException {
133         HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
134         http.setRequestMethod("POST");
135         http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
136         http.setRequestProperty("Accept", "*/*");
137         if ((session != null) && !session.isEmpty()) {
138             http.setRequestProperty("Cookie", String.format("_SESSION=%s", session));
139         }
140         http.setDoOutput(true);
141         http.setDoInput(true);
142
143         UrlEncodedFormData params = new UrlEncodedFormData();
144         params.addPair("_formid", "identify-add");
145         if (token != null)
146             params.addPair("_token", token);
147         params.addPair("date", Globals.formatLedgerDate(ltr.getDate()));
148         params.addPair("description", ltr.getDescription());
149         for (LedgerTransactionAccount acc : ltr.getAccounts()) {
150             params.addPair("account", acc.getAccountName());
151             if (acc.isAmountSet())
152                 params.addPair("amount", String.format(Locale.US, "%1.2f", acc.getAmount()));
153             else
154                 params.addPair("amount", "");
155         }
156
157         String body = params.toString();
158         http.addRequestProperty("Content-Length", String.valueOf(body.length()));
159
160         debug("network", "request header: " + http.getRequestProperties()
161                                                   .toString());
162
163         try (OutputStream req = http.getOutputStream()) {
164             debug("network", "Request body: " + body);
165             req.write(body.getBytes(StandardCharsets.US_ASCII));
166
167             try (InputStream resp = http.getInputStream()) {
168                 debug("update_accounts", String.valueOf(http.getResponseCode()));
169                 if (http.getResponseCode() == 303) {
170                     // everything is fine
171                     return true;
172                 }
173                 else if (http.getResponseCode() == 200) {
174                     // get the new cookie
175                     {
176                         Pattern reSessionCookie = Pattern.compile("_SESSION=([^;]+);.*");
177
178                         Map<String, List<String>> header = http.getHeaderFields();
179                         List<String> cookieHeader = header.get("Set-Cookie");
180                         if (cookieHeader != null) {
181                             String cookie = cookieHeader.get(0);
182                             Matcher m = reSessionCookie.matcher(cookie);
183                             if (m.matches()) {
184                                 session = m.group(1);
185                                 debug("network", "new session is " + session);
186                             }
187                             else {
188                                 debug("network", "set-cookie: " + cookie);
189                                 Log.w("network",
190                                         "Response Set-Cookie headers is not a _SESSION one");
191                             }
192                         }
193                         else {
194                             Log.w("network", "Response has no Set-Cookie header");
195                         }
196                     }
197                     // the token needs to be updated
198                     BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
199                     Pattern re = Pattern.compile(
200                             "<input type=\"hidden\" name=\"_token\" value=\"([^\"]+)\">");
201                     String line;
202                     while ((line = reader.readLine()) != null) {
203                         //debug("dump", line);
204                         Matcher m = re.matcher(line);
205                         if (m.matches()) {
206                             token = m.group(1);
207                             debug("save-transaction", line);
208                             debug("save-transaction", "Token=" + token);
209                             return false;       // retry
210                         }
211                     }
212                     throw new IOException("Can't find _token string");
213                 }
214                 else {
215                     throw new IOException(
216                             String.format("Error response code %d", http.getResponseCode()));
217                 }
218             }
219         }
220     }
221
222     @Override
223     protected Void doInBackground(LedgerTransaction... ledgerTransactions) {
224         error = null;
225         try {
226             ltr = ledgerTransactions[0];
227
228             if (!sendOK()) {
229                 int tried = 0;
230                 while (!legacySendOK()) {
231                     tried++;
232                     if (tried >= 2)
233                         throw new IOException(String.format("aborting after %d tries", tried));
234                     sleep(100);
235                 }
236             }
237         }
238         catch (Exception e) {
239             e.printStackTrace();
240             error = e.getMessage();
241         }
242
243         return null;
244     }
245
246     @Override
247     protected void onPostExecute(Void aVoid) {
248         super.onPostExecute(aVoid);
249         taskCallback.done(error);
250     }
251 }