7 package com.google.appinventor.components.runtime;
9 import android.Manifest;
10 import android.app.Activity;
12 import android.text.TextUtils;
14 import android.util.Log;
16 import androidx.annotation.VisibleForTesting;
54 import java.io.BufferedInputStream;
55 import java.io.BufferedOutputStream;
57 import java.io.FileOutputStream;
58 import java.io.IOException;
59 import java.io.InputStream;
60 import java.io.InputStreamReader;
61 import java.io.StringReader;
62 import java.io.UnsupportedEncodingException;
64 import java.net.CookieHandler;
65 import java.net.HttpURLConnection;
66 import java.net.MalformedURLException;
67 import java.net.ProtocolException;
68 import java.net.SocketTimeoutException;
69 import java.net.URISyntaxException;
71 import java.net.URLDecoder;
72 import java.net.URLEncoder;
74 import java.util.List;
77 import javax.xml.parsers.SAXParser;
78 import javax.xml.parsers.SAXParserFactory;
80 import org.json.JSONException;
82 import org.xml.sax.InputSource;
90 @DesignerComponent(version = YaVersion.WEB_COMPONENT_VERSION,
91 description =
"Non-visible component that provides functions for HTTP GET, POST, PUT, and DELETE requests.",
92 category = ComponentCategory.CONNECTIVITY,
94 iconName =
"images/web.png")
96 @UsesPermissions(permissionNames =
"android.permission.INTERNET," +
97 "android.permission.WRITE_EXTERNAL_STORAGE," +
98 "android.permission.READ_EXTERNAL_STORAGE")
99 @UsesLibraries(libraries =
"json.jar")
109 private static class InvalidRequestHeadersException
extends Exception {
115 final int errorNumber;
118 InvalidRequestHeadersException(
int errorNumber,
int index) {
120 this.errorNumber = errorNumber;
132 static class BuildRequestDataException
extends Exception {
138 final int errorNumber;
141 BuildRequestDataException(
int errorNumber,
int index) {
143 this.errorNumber = errorNumber;
153 private static class CapturedProperties {
154 final String urlString;
156 final boolean allowCookies;
157 final boolean saveResponse;
158 final String responseFileName;
163 CapturedProperties(
Web web)
throws MalformedURLException, InvalidRequestHeadersException {
164 urlString = web.urlString;
165 url =
new URL(urlString);
166 allowCookies = web.allowCookies;
167 saveResponse = web.saveResponse;
168 responseFileName = web.responseFileName;
169 timeout = web.timeout;
170 requestHeaders = processRequestHeaders(web.requestHeaders);
173 if (allowCookies && web.cookieHandler !=
null) {
175 cookiesTemp = web.cookieHandler.get(url.toURI(), requestHeaders);
176 }
catch (URISyntaxException e) {
178 }
catch (IOException e) {
182 cookies = cookiesTemp;
186 private static final String LOG_TAG =
"Web";
191 mimeTypeToExtension.put(
"application/pdf",
"pdf");
192 mimeTypeToExtension.put(
"application/zip",
"zip");
193 mimeTypeToExtension.put(
"audio/mpeg",
"mpeg");
194 mimeTypeToExtension.put(
"audio/mp3",
"mp3");
195 mimeTypeToExtension.put(
"audio/mp4",
"mp4");
196 mimeTypeToExtension.put(
"image/gif",
"gif");
197 mimeTypeToExtension.put(
"image/jpeg",
"jpg");
198 mimeTypeToExtension.put(
"image/png",
"png");
199 mimeTypeToExtension.put(
"image/tiff",
"tiff");
200 mimeTypeToExtension.put(
"text/plain",
"txt");
201 mimeTypeToExtension.put(
"text/html",
"html");
202 mimeTypeToExtension.put(
"text/xml",
"xml");
206 private final Activity activity;
207 private final CookieHandler cookieHandler;
209 private String urlString =
"";
210 private boolean allowCookies;
212 private boolean saveResponse;
213 private String responseFileName =
"";
214 private int timeout = 0;
218 private boolean havePermission =
false;
228 super(container.
$form());
242 cookieHandler =
null;
251 description =
"The URL for the web request.")
252 public String Url() {
262 public void Url(String url) {
274 description =
"The request headers, as a list of two-element sublists. The first element " +
275 "of each sublist represents the request header field name. The second element of each " +
276 "sublist represents the request header field values, either a single value or a list " +
277 "containing multiple values.")
279 return requestHeaders;
292 processRequestHeaders(list);
293 requestHeaders = list;
294 }
catch (InvalidRequestHeadersException e) {
295 form.dispatchErrorOccurredEvent(
this,
"RequestHeaders", e.errorNumber, e.index);
306 description =
"Whether the cookies from a response should be saved and used in subsequent " +
307 "requests. Cookies are only supported on Android version 2.3 or greater.")
308 public
boolean AllowCookies() {
316 defaultValue =
"false")
319 this.allowCookies = allowCookies;
320 if (allowCookies && cookieHandler ==
null) {
321 form.dispatchErrorOccurredEvent(
this,
"AllowCookies",
330 description =
"Whether the response should be saved in a file.")
331 public
boolean SaveResponse() {
339 defaultValue =
"false")
342 this.saveResponse = saveResponse;
352 description =
"The name of the file where the response should be saved. If SaveResponse " +
353 "is true and ResponseFileName is empty, then a new file name will be generated.")
354 public String ResponseFileName() {
355 return responseFileName;
367 this.responseFileName = responseFileName;
375 description =
"The number of milliseconds that a web request will wait for a response before giving up. " +
376 "If set to 0, then there is no time limit on how long the request will wait.")
377 public
int Timeout() {
392 this.timeout = timeout;
395 @
SimpleFunction(description =
"Clears all cookies for this Web component.")
396 public
void ClearCookies() {
397 if (cookieHandler !=
null) {
400 form.dispatchErrorOccurredEvent(
this,
"ClearCookies",
418 final String METHOD =
"Get";
420 final CapturedProperties webProps = capturePropertyValues(METHOD);
421 if (webProps ==
null) {
429 performRequest(webProps,
null,
null,
"GET", METHOD);
447 @
SimpleFunction(description =
"Performs an HTTP POST request using the Url property and " +
448 "the specified text.<br>" +
449 "The characters of the text are encoded using UTF-8 encoding.<br>" +
450 "If the SaveResponse property is true, the response will be saved in a file and the " +
451 "GotFile event will be triggered. The responseFileName property can be used to specify " +
452 "the name of the file.<br>" +
453 "If the SaveResponse property is false, the GotText event will be triggered.")
454 public
void PostText(final String text) {
455 requestTextImpl(text,
"UTF-8",
"PostText",
"POST");
473 @
SimpleFunction(description =
"Performs an HTTP POST request using the Url property and " +
474 "the specified text.<br>" +
475 "The characters of the text are encoded using the given encoding.<br>" +
476 "If the SaveResponse property is true, the response will be saved in a file and the " +
477 "GotFile event will be triggered. The ResponseFileName property can be used to specify " +
478 "the name of the file.<br>" +
479 "If the SaveResponse property is false, the GotText event will be triggered.")
480 public
void PostTextWithEncoding(final String text, final String encoding) {
481 requestTextImpl(text, encoding,
"PostTextWithEncoding",
"POST");
495 @
SimpleFunction(description =
"Performs an HTTP POST request using the Url property and " +
496 "data from the specified file.<br>" +
497 "If the SaveResponse property is true, the response will be saved in a file and the " +
498 "GotFile event will be triggered. The ResponseFileName property can be used to specify " +
499 "the name of the file.<br>" +
500 "If the SaveResponse property is false, the GotText event will be triggered.")
501 public
void PostFile(final String path) {
502 final String METHOD =
"PostFile";
504 final CapturedProperties webProps = capturePropertyValues(METHOD);
505 if (webProps ==
null) {
513 performRequest(webProps,
null, path,
"POST", METHOD);
531 @
SimpleFunction(description =
"Performs an HTTP PUT request using the Url property and " +
532 "the specified text.<br>" +
533 "The characters of the text are encoded using UTF-8 encoding.<br>" +
534 "If the SaveResponse property is true, the response will be saved in a file and the " +
535 "GotFile event will be triggered. The responseFileName property can be used to specify " +
536 "the name of the file.<br>" +
537 "If the SaveResponse property is false, the GotText event will be triggered.")
538 public
void PutText(final String text) {
539 requestTextImpl(text,
"UTF-8",
"PutText",
"PUT");
557 @
SimpleFunction(description =
"Performs an HTTP PUT request using the Url property and " +
558 "the specified text.<br>" +
559 "The characters of the text are encoded using the given encoding.<br>" +
560 "If the SaveResponse property is true, the response will be saved in a file and the " +
561 "GotFile event will be triggered. The ResponseFileName property can be used to specify " +
562 "the name of the file.<br>" +
563 "If the SaveResponse property is false, the GotText event will be triggered.")
564 public
void PutTextWithEncoding(final String text, final String encoding) {
565 requestTextImpl(text, encoding,
"PutTextWithEncoding",
"PUT");
579 @
SimpleFunction(description =
"Performs an HTTP PUT request using the Url property and " +
580 "data from the specified file.<br>" +
581 "If the SaveResponse property is true, the response will be saved in a file and the " +
582 "GotFile event will be triggered. The ResponseFileName property can be used to specify " +
583 "the name of the file.<br>" +
584 "If the SaveResponse property is false, the GotText event will be triggered.")
585 public
void PutFile(final String path) {
586 final String METHOD =
"PutFile";
588 final CapturedProperties webProps = capturePropertyValues(METHOD);
589 if (webProps ==
null) {
597 performRequest(webProps,
null, path,
"PUT", METHOD);
615 final String METHOD =
"Delete";
617 final CapturedProperties webProps = capturePropertyValues(METHOD);
618 if (webProps ==
null) {
626 performRequest(webProps,
null,
null,
"DELETE", METHOD);
647 private void requestTextImpl(
final String text,
final String encoding,
648 final String functionName,
final String httpVerb) {
650 final CapturedProperties webProps = capturePropertyValues(functionName);
651 if (webProps ==
null) {
656 AsynchUtil.runAsynchronously(
new Runnable() {
662 if (encoding ==
null || encoding.length() == 0) {
663 requestData = text.getBytes(
"UTF-8");
665 requestData = text.getBytes(encoding);
667 }
catch (UnsupportedEncodingException e) {
668 form.dispatchErrorOccurredEvent(Web.this, functionName,
669 ErrorMessages.ERROR_WEB_UNSUPPORTED_ENCODING, encoding);
673 performRequest(webProps, requestData,
null, httpVerb, functionName);
687 public void GotText(String url,
int responseCode, String responseType, String responseContent) {
702 public void GotFile(String url,
int responseCode, String responseType, String fileName) {
728 return buildRequestData(list);
729 }
catch (BuildRequestDataException e) {
730 form.dispatchErrorOccurredEvent(
this,
"BuildRequestData", e.errorNumber, e.index);
744 String buildRequestData(
YailList list)
throws BuildRequestDataException {
745 StringBuilder sb =
new StringBuilder();
746 String delimiter =
"";
747 for (
int i = 0; i < list.size(); i++) {
748 Object item = list.getObject(i);
752 if (sublist.
size() == 2) {
754 String name = sublist.
getObject(0).toString();
756 String value = sublist.
getObject(1).toString();
757 sb.append(delimiter).append(UriEncode(name)).append(
'=').append(UriEncode(value));
759 throw new BuildRequestDataException(
760 ErrorMessages.ERROR_WEB_BUILD_REQUEST_DATA_NOT_TWO_ELEMENTS, i + 1);
763 throw new BuildRequestDataException(ErrorMessages.ERROR_WEB_BUILD_REQUEST_DATA_NOT_LIST, i + 1);
767 return sb.toString();
779 return URLEncoder.encode(text,
"UTF-8");
780 }
catch (UnsupportedEncodingException e) {
784 Log.e(LOG_TAG,
"UTF-8 is unsupported?", e);
799 return URLDecoder.decode(text,
"UTF-8");
800 }
catch (UnsupportedEncodingException e) {
804 Log.e(LOG_TAG,
"UTF-8 is unsupported?", e);
832 return decodeJsonText(jsonText,
false);
833 }
catch (IllegalArgumentException e) {
834 form.dispatchErrorOccurredEvent(
this,
"JsonTextDecode",
851 return decodeJsonText(jsonText,
true);
852 }
catch (IllegalArgumentException e) {
853 form.dispatchErrorOccurredEvent(
this,
"JsonTextDecodeWithDictionaries",
869 static Object decodeJsonText(String jsonText,
boolean useDicts)
throws IllegalArgumentException {
872 }
catch (JSONException e) {
873 throw new IllegalArgumentException(
"jsonText is not a legal JSON value");
887 static Object decodeJsonText(String jsonText)
throws IllegalArgumentException {
888 return decodeJsonText(jsonText,
false);
904 }
catch (IllegalArgumentException e) {
905 form.dispatchErrorOccurredEvent(
this,
"JsonObjectEncode",
947 @
SimpleFunction(description =
"Decodes the given XML into a set of nested dictionaries that " +
948 "capture the structure and data contained in the XML. See the help for more details.") public Object XMLTextDecodeAsDictionary(String XmlText) {
951 SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
952 InputSource is =
new InputSource(
new StringReader(XmlText));
953 is.setEncoding(
"UTF-8");
956 }
catch (Exception e) {
957 Log.e(LOG_TAG, e.getMessage());
958 form.dispatchErrorOccurredEvent(
this,
"XMLTextDecodeAsDictionary",
988 @
SimpleFunction(description =
"Decodes the given XML string to produce a dictionary structure. " +
989 "See the App Inventor documentation on \"Other topics, notes, and details\" for information.")
993 public Object XMLTextDecode(String XmlText) {
995 return JsonTextDecode(XML.toJSONObject(XmlText).toString());
1000 Log.e(LOG_TAG, e.getMessage());
1001 form.dispatchErrorOccurredEvent(
this,
"XMLTextDecode",
1018 @
SimpleFunction(description =
"Decodes the given HTML text value. HTML character entities " +
1019 "such as &amp;, &lt;, &gt;, &apos;, and &quot; are changed to " +
1020 "&, <, >, ', and ". Entities such as &#xhhhh, and &#nnnn " +
1021 "are changed to the appropriate characters.")
1022 public String HtmlTextDecode(String htmlText) {
1025 }
catch (IllegalArgumentException e) {
1026 form.dispatchErrorOccurredEvent(
this,
"HtmlTextDecode",
1054 private void performRequest(
final CapturedProperties webProps,
final byte[] postData,
1055 final String postFile,
final String httpVerb,
final String method) {
1058 if (saveResponse & !havePermission) {
1059 final Web me =
this;
1061 Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
1063 public void onGranted() {
1064 me.havePermission = true;
1067 AsynchUtil.runAsynchronously(new Runnable() {
1070 me.performRequest(webProps, postData, postFile, httpVerb, method);
1080 HttpURLConnection connection = openConnection(webProps, httpVerb);
1081 if (connection !=
null) {
1083 if (postData !=
null) {
1084 writeRequestData(connection, postData);
1085 }
else if (postFile !=
null) {
1086 writeRequestFile(connection, postFile);
1090 final int responseCode = connection.getResponseCode();
1091 final String responseType = getResponseType(connection);
1092 processResponseCookies(connection);
1095 final String path = saveResponseContent(connection, webProps.responseFileName,
1099 activity.runOnUiThread(
new Runnable() {
1102 GotFile(webProps.urlString, responseCode, responseType, path);
1106 final String responseContent = getResponseContent(connection);
1109 activity.runOnUiThread(
new Runnable() {
1112 GotText(webProps.urlString, responseCode, responseType, responseContent);
1117 }
catch (SocketTimeoutException e) {
1119 activity.runOnUiThread(
new Runnable() {
1122 TimedOut(webProps.urlString);
1125 throw new RequestTimeoutException();
1127 connection.disconnect();
1130 }
catch (PermissionException e) {
1131 form.dispatchPermissionDeniedEvent(Web.this, method, e);
1132 }
catch (FileUtil.FileException e) {
1133 form.dispatchErrorOccurredEvent(Web.this, method,
1134 e.getErrorMessageNumber());
1135 }
catch (RequestTimeoutException e) {
1136 form.dispatchErrorOccurredEvent(Web.this, method,
1137 ErrorMessages.ERROR_WEB_REQUEST_TIMED_OUT, webProps.urlString);
1138 }
catch (Exception e) {
1140 if (method.equals(
"Get")) {
1141 message = ErrorMessages.ERROR_WEB_UNABLE_TO_GET;
1142 }
else if (method.equals(
"PostFile")) {
1143 message = ErrorMessages.ERROR_WEB_UNABLE_TO_POST_OR_PUT_FILE;
1144 }
else if (method.equals(
"PutFile")) {
1145 message = ErrorMessages.ERROR_WEB_UNABLE_TO_POST_OR_PUT_FILE;
1146 }
else if (method.equals(
"Delete")) {
1147 message = ErrorMessages.ERROR_WEB_UNABLE_TO_DELETE;
1149 message = ErrorMessages.ERROR_WEB_UNABLE_TO_POST_OR_PUT;
1151 form.dispatchErrorOccurredEvent(Web.this, method,
1152 message, webProps.urlString);
1166 private static HttpURLConnection openConnection(CapturedProperties webProps, String httpVerb)
1167 throws IOException, ClassCastException, ProtocolException {
1169 HttpURLConnection connection = (HttpURLConnection) webProps.url.openConnection();
1170 connection.setConnectTimeout(webProps.timeout);
1171 connection.setReadTimeout(webProps.timeout);
1173 if (httpVerb.equals(
"PUT") || httpVerb.equals(
"DELETE")){
1176 connection.setRequestMethod(httpVerb);
1180 for (Map.Entry<String, List<String>> header : webProps.requestHeaders.entrySet()) {
1181 String name = header.getKey();
1182 for (String value : header.getValue()) {
1183 connection.addRequestProperty(name, value);
1188 if (webProps.cookies !=
null) {
1189 for (Map.Entry<String, List<String>> cookie : webProps.cookies.entrySet()) {
1190 String name = cookie.getKey();
1191 for (String value : cookie.getValue()) {
1192 connection.addRequestProperty(name, value);
1200 private static void writeRequestData(HttpURLConnection connection,
byte[] postData)
1201 throws IOException {
1206 connection.setDoOutput(
true);
1208 connection.setFixedLengthStreamingMode(postData.length);
1209 BufferedOutputStream out =
new BufferedOutputStream(connection.getOutputStream());
1211 out.write(postData, 0, postData.length);
1218 private void writeRequestFile(HttpURLConnection connection, String path)
1219 throws IOException {
1222 BufferedInputStream in =
new BufferedInputStream(MediaUtil.openMedia(form, path));
1229 connection.setDoOutput(
true);
1230 connection.setChunkedStreamingMode(0);
1231 BufferedOutputStream out =
new BufferedOutputStream(connection.getOutputStream());
1249 private static String getResponseType(HttpURLConnection connection) {
1250 String responseType = connection.getContentType();
1251 return (responseType !=
null) ? responseType :
"";
1254 private void processResponseCookies(HttpURLConnection connection) {
1255 if (allowCookies && cookieHandler !=
null) {
1257 Map<String, List<String>> headerFields = connection.getHeaderFields();
1258 cookieHandler.put(connection.getURL().toURI(), headerFields);
1259 }
catch (URISyntaxException e) {
1261 }
catch (IOException e) {
1267 private static String getResponseContent(HttpURLConnection connection)
throws IOException {
1269 String encoding = connection.getContentEncoding();
1270 if (encoding ==
null) {
1273 InputStreamReader reader =
new InputStreamReader(getConnectionStream(connection), encoding);
1275 int contentLength = connection.getContentLength();
1276 StringBuilder sb = (contentLength != -1)
1277 ?
new StringBuilder(contentLength)
1278 :
new StringBuilder();
1279 char[] buf =
new char[1024];
1281 while ((read = reader.read(buf)) != -1) {
1282 sb.append(buf, 0, read);
1284 return sb.toString();
1290 private String saveResponseContent(HttpURLConnection connection,
1291 String responseFileName, String responseType)
throws IOException {
1292 File file = createFile(responseFileName, responseType);
1294 BufferedInputStream in =
new BufferedInputStream(getConnectionStream(connection), 0x1000);
1296 BufferedOutputStream out =
new BufferedOutputStream(
new FileOutputStream(file), 0x1000);
1314 return file.getAbsolutePath();
1317 private static InputStream getConnectionStream(HttpURLConnection connection)
throws SocketTimeoutException {
1322 return connection.getInputStream();
1323 }
catch (SocketTimeoutException e) {
1325 }
catch (IOException e1) {
1327 return connection.getErrorStream();
1331 private File createFile(String fileName, String responseType)
1332 throws IOException, FileUtil.FileException {
1334 if (!TextUtils.isEmpty(fileName)) {
1335 return FileUtil.getExternalFile(form, fileName);
1341 int indexOfSemicolon = responseType.indexOf(
';');
1342 if (indexOfSemicolon != -1) {
1343 responseType = responseType.substring(0, indexOfSemicolon);
1345 String extension = mimeTypeToExtension.get(responseType);
1346 if (extension ==
null) {
1349 return FileUtil.getDownloadFile(form, extension);
1357 private static Map<String, List<String>> processRequestHeaders(YailList list)
1358 throws InvalidRequestHeadersException {
1359 Map<String, List<String>> requestHeadersMap = Maps.newHashMap();
1360 for (
int i = 0; i < list.size(); i++) {
1361 Object item = list.getObject(i);
1363 if (item instanceof YailList) {
1364 YailList sublist = (YailList) item;
1365 if (sublist.size() == 2) {
1367 String fieldName = sublist.
getObject(0).toString();
1369 Object fieldValues = sublist.getObject(1);
1372 String key = fieldName;
1373 List<String> values = Lists.newArrayList();
1378 if (fieldValues instanceof YailList) {
1380 YailList multipleFieldsValues = (YailList) fieldValues;
1381 for (
int j = 0; j < multipleFieldsValues.size(); j++) {
1382 Object value = multipleFieldsValues.getObject(j);
1383 values.add(value.toString());
1387 Object singleFieldValue = fieldValues;
1388 values.add(singleFieldValue.toString());
1391 requestHeadersMap.put(key, values);
1394 throw new InvalidRequestHeadersException(
1395 ErrorMessages.ERROR_WEB_REQUEST_HEADER_NOT_TWO_ELEMENTS, i + 1);
1399 throw new InvalidRequestHeadersException(
1400 ErrorMessages.ERROR_WEB_REQUEST_HEADER_NOT_LIST, i + 1);
1403 return requestHeadersMap;
1413 private CapturedProperties capturePropertyValues(String functionName) {
1415 return new CapturedProperties(
this);
1416 }
catch (MalformedURLException e) {
1417 form.dispatchErrorOccurredEvent(
this, functionName,
1418 ErrorMessages.ERROR_WEB_MALFORMED_URL, urlString);
1419 }
catch (InvalidRequestHeadersException e) {
1420 form.dispatchErrorOccurredEvent(
this, functionName, e.errorNumber, e.index);