AI2 Component  (Version nb184)
Web.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright 2009-2011 Google, All Rights reserved
3 // Copyright 2011-2020 MIT, All rights reserved
4 // Released under the Apache License, Version 2.0
5 // http://www.apache.org/licenses/LICENSE-2.0
6 
7 package com.google.appinventor.components.runtime;
8 
9 import android.Manifest;
10 import android.app.Activity;
11 
12 import android.text.TextUtils;
13 
14 import android.util.Log;
15 
16 import androidx.annotation.VisibleForTesting;
17 
27 
32 
35 
39 
40 import com.google.appinventor.components.runtime.repackaged.org.json.XML;
41 
53 
54 import java.io.BufferedInputStream;
55 import java.io.BufferedOutputStream;
56 import java.io.File;
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;
63 
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;
70 import java.net.URL;
71 import java.net.URLDecoder;
72 import java.net.URLEncoder;
73 
74 import java.util.List;
75 import java.util.Map;
76 
77 import javax.xml.parsers.SAXParser;
78 import javax.xml.parsers.SAXParserFactory;
79 
80 import org.json.JSONException;
81 
82 import org.xml.sax.InputSource;
83 
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,
93  nonVisible = true,
94  iconName = "images/web.png")
95 @SimpleObject
96 @UsesPermissions(permissionNames = "android.permission.INTERNET," +
97  "android.permission.WRITE_EXTERNAL_STORAGE," +
98  "android.permission.READ_EXTERNAL_STORAGE")
99 @UsesLibraries(libraries = "json.jar")
100 
101 
102 public class Web extends AndroidNonvisibleComponent implements Component {
109  private static class InvalidRequestHeadersException extends Exception {
110  /*
111  * errorNumber could be:
112  * ErrorMessages.ERROR_WEB_REQUEST_HEADER_NOT_LIST
113  * ErrorMessages.ERROR_WEB_REQUEST_HEADER_NOT_TWO_ELEMENTS
114  */
115  final int errorNumber;
116  final int index; // the index of the invalid header
117 
118  InvalidRequestHeadersException(int errorNumber, int index) {
119  super();
120  this.errorNumber = errorNumber;
121  this.index = index;
122  }
123  }
124 
131  // VisibleForTesting
132  static class BuildRequestDataException extends Exception {
133  /*
134  * errorNumber could be:
135  * ErrorMessages.ERROR_WEB_BUILD_REQUEST_DATA_NOT_LIST
136  * ErrorMessages.ERROR_WEB_BUILD_REQUEST_DATA_NOT_TWO_ELEMENTS
137  */
138  final int errorNumber;
139  final int index; // the index of the invalid header
140 
141  BuildRequestDataException(int errorNumber, int index) {
142  super();
143  this.errorNumber = errorNumber;
144  this.index = index;
145  }
146  }
147 
153  private static class CapturedProperties {
154  final String urlString;
155  final URL url;
156  final boolean allowCookies;
157  final boolean saveResponse;
158  final String responseFileName;
159  final int timeout;
160  final Map<String, List<String>> requestHeaders;
161  final Map<String, List<String>> cookies;
162 
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);
171 
172  Map<String, List<String>> cookiesTemp = null;
173  if (allowCookies && web.cookieHandler != null) {
174  try {
175  cookiesTemp = web.cookieHandler.get(url.toURI(), requestHeaders);
176  } catch (URISyntaxException e) {
177  // Can't convert the URL to a URI; no cookies for you.
178  } catch (IOException e) {
179  // Sorry, no cookies for you.
180  }
181  }
182  cookies = cookiesTemp;
183  }
184  }
185 
186  private static final String LOG_TAG = "Web";
187 
188  private static final Map<String, String> mimeTypeToExtension;
189  static {
190  mimeTypeToExtension = Maps.newHashMap();
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");
203  // TODO(lizlooney) - consider adding more mime types.
204  }
205 
206  private final Activity activity;
207  private final CookieHandler cookieHandler;
208 
209  private String urlString = "";
210  private boolean allowCookies;
211  private YailList requestHeaders = new YailList();
212  private boolean saveResponse;
213  private String responseFileName = "";
214  private int timeout = 0;
215 
216  // wether or not we have permission to manipulate external storage
217 
218  private boolean havePermission = false;
219 
220 
221 
227  public Web(ComponentContainer container) {
228  super(container.$form());
229  activity = container.$context();
230 
231  cookieHandler = (SdkLevel.getLevel() >= SdkLevel.LEVEL_GINGERBREAD)
233  : null;
234  }
235 
239  protected Web() {
240  super(null);
241  activity = null;
242  cookieHandler = null;
243  }
244 
251  description = "The URL for the web request.")
252  public String Url() {
253  return urlString;
254  }
255 
260  defaultValue = "")
262  public void Url(String url) {
263  urlString = url;
264  }
265 
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.")
278  public YailList RequestHeaders() {
279  return requestHeaders;
280  }
281 
288  public void RequestHeaders(YailList list) {
289  // Call processRequestHeaders to validate the list parameter before setting the requestHeaders
290  // field.
291  try {
292  processRequestHeaders(list);
293  requestHeaders = list;
294  } catch (InvalidRequestHeadersException e) {
295  form.dispatchErrorOccurredEvent(this, "RequestHeaders", e.errorNumber, e.index);
296  }
297  }
298 
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() {
309  return allowCookies;
310  }
311 
316  defaultValue = "false")
318  public void AllowCookies(boolean allowCookies) {
319  this.allowCookies = allowCookies;
320  if (allowCookies && cookieHandler == null) {
321  form.dispatchErrorOccurredEvent(this, "AllowCookies",
323  }
324  }
325 
330  description = "Whether the response should be saved in a file.")
331  public boolean SaveResponse() {
332  return saveResponse;
333  }
334 
339  defaultValue = "false")
341  public void SaveResponse(boolean saveResponse) {
342  this.saveResponse = saveResponse;
343  }
344 
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;
356  }
357 
364  defaultValue = "")
366  public void ResponseFileName(String responseFileName) {
367  this.responseFileName = responseFileName;
368  }
369 
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() {
378  return timeout;
379  }
380 
386  defaultValue = "0")
388  public void Timeout(int timeout) {
389  if (timeout < 0){
390  throw new IllegalArgumentError("Web Timeout must be a non-negative integer.");
391  }
392  this.timeout = timeout;
393  }
394 
395  @SimpleFunction(description = "Clears all cookies for this Web component.")
396  public void ClearCookies() {
397  if (cookieHandler != null) {
398  GingerbreadUtil.clearCookies(cookieHandler);
399  } else {
400  form.dispatchErrorOccurredEvent(this, "ClearCookies",
402  }
403  }
404 
417  public void Get() {
418  final String METHOD = "Get";
419  // Capture property values in local variables before running asynchronously.
420  final CapturedProperties webProps = capturePropertyValues(METHOD);
421  if (webProps == null) {
422  // capturePropertyValues has already called form.dispatchErrorOccurredEvent
423  return;
424  }
425 
426  AsynchUtil.runAsynchronously(new Runnable() {
427  @Override
428  public void run() {
429  performRequest(webProps, null, null, "GET", METHOD);
430  }
431  });
432  }
433 
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");
456  }
457 
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");
482  }
483 
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";
503  // Capture property values before running asynchronously.
504  final CapturedProperties webProps = capturePropertyValues(METHOD);
505  if (webProps == null) {
506  // capturePropertyValues has already called form.dispatchErrorOccurredEvent
507  return;
508  }
509 
510  AsynchUtil.runAsynchronously(new Runnable() {
511  @Override
512  public void run() {
513  performRequest(webProps, null, path, "POST", METHOD);
514  }
515  });
516  }
517 
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");
540  }
541 
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");
566  }
567 
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";
587  // Capture property values before running asynchronously.
588  final CapturedProperties webProps = capturePropertyValues(METHOD);
589  if (webProps == null) {
590  // capturePropertyValues has already called form.dispatchErrorOccurredEvent
591  return;
592  }
593 
594  AsynchUtil.runAsynchronously(new Runnable() {
595  @Override
596  public void run() {
597  performRequest(webProps, null, path, "PUT", METHOD);
598  }
599  });
600  }
601 
614  public void Delete() {
615  final String METHOD = "Delete";
616  // Capture property values in local variables before running asynchronously.
617  final CapturedProperties webProps = capturePropertyValues(METHOD);
618  if (webProps == null) {
619  // capturePropertyValues has already called form.dispatchErrorOccurredEvent
620  return;
621  }
622 
623  AsynchUtil.runAsynchronously(new Runnable() {
624  @Override
625  public void run() {
626  performRequest(webProps, null, null, "DELETE", METHOD);
627  }
628  });
629  }
630 
631  /*
632  * Performs an HTTP GET, POST, PUT or DELETE request using the Url property and the specified
633  * text, and retrieves the response asynchronously.<br>
634  * The characters of the text are encoded using the given encoding.<br>
635  * If the SaveResponse property is true, the response will be saved in a file
636  * and the GotFile event will be triggered. The ResponseFileName property
637  * can be used to specify the name of the file.<br>
638  * If the SaveResponse property is false, the GotText event will be
639  * triggered.
640  *
641  * @param text the text data for the POST or PUT request
642  * @param encoding the character encoding to use when sending the text. If
643  * encoding is empty or null, UTF-8 encoding will be used.
644  * @param functionName the name of the function, used when dispatching errors
645  * @param httpVerb the HTTP operation to be performed: GET, POST, PUT or DELETE
646  */
647  private void requestTextImpl(final String text, final String encoding,
648  final String functionName, final String httpVerb) {
649  // Capture property values before running asynchronously.
650  final CapturedProperties webProps = capturePropertyValues(functionName);
651  if (webProps == null) {
652  // capturePropertyValues has already called form.dispatchErrorOccurredEvent
653  return;
654  }
655 
656  AsynchUtil.runAsynchronously(new Runnable() {
657  @Override
658  public void run() {
659  // Convert text to bytes using the encoding.
660  byte[] requestData;
661  try {
662  if (encoding == null || encoding.length() == 0) {
663  requestData = text.getBytes("UTF-8");
664  } else {
665  requestData = text.getBytes(encoding);
666  }
667  } catch (UnsupportedEncodingException e) {
668  form.dispatchErrorOccurredEvent(Web.this, functionName,
669  ErrorMessages.ERROR_WEB_UNSUPPORTED_ENCODING, encoding);
670  return;
671  }
672 
673  performRequest(webProps, requestData, null, httpVerb, functionName);
674  }
675  });
676  }
677 
686  @SimpleEvent
687  public void GotText(String url, int responseCode, String responseType, String responseContent) {
688  // invoke the application's "GotText" event handler.
689  EventDispatcher.dispatchEvent(this, "GotText", url, responseCode, responseType,
690  responseContent);
691  }
692 
701  @SimpleEvent
702  public void GotFile(String url, int responseCode, String responseType, String fileName) {
703  // invoke the application's "GotFile" event handler.
704  EventDispatcher.dispatchEvent(this, "GotFile", url, responseCode, responseType, fileName);
705  }
706 
712  @SimpleEvent
713  public void TimedOut(String url) {
714  // invoke the application's "TimedOut" event handler.
715  EventDispatcher.dispatchEvent(this, "TimedOut", url);
716  }
717 
726  public String BuildRequestData(YailList list) {
727  try {
728  return buildRequestData(list);
729  } catch (BuildRequestDataException e) {
730  form.dispatchErrorOccurredEvent(this, "BuildRequestData", e.errorNumber, e.index);
731  return "";
732  }
733  }
734 
735  /*
736  * Converts a list of two-element sublists, representing name and value pairs, to a
737  * string formatted as application/x-www-form-urlencoded media type, suitable to pass to
738  * PostText.
739  *
740  * @param list a list of two-element sublists representing name and value pairs
741  * @throws BuildPostDataException if the list is not valid
742  */
743  // VisibleForTesting
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);
749  // Each item must be a two-element sublist.
750  if (item instanceof YailList) {
751  YailList sublist = (YailList) item;
752  if (sublist.size() == 2) {
753  // The first element is the name.
754  String name = sublist.getObject(0).toString();
755  // The second element is the value.
756  String value = sublist.getObject(1).toString();
757  sb.append(delimiter).append(UriEncode(name)).append('=').append(UriEncode(value));
758  } else {
759  throw new BuildRequestDataException(
760  ErrorMessages.ERROR_WEB_BUILD_REQUEST_DATA_NOT_TWO_ELEMENTS, i + 1);
761  }
762  } else {
763  throw new BuildRequestDataException(ErrorMessages.ERROR_WEB_BUILD_REQUEST_DATA_NOT_LIST, i + 1);
764  }
765  delimiter = "&";
766  }
767  return sb.toString();
768  }
769 
776  @SimpleFunction
777  public String UriEncode(String text) {
778  try {
779  return URLEncoder.encode(text, "UTF-8");
780  } catch (UnsupportedEncodingException e) {
781  // If UTF-8 is not supported, we're in big trouble!
782  // According to Javadoc and Android documentation for java.nio.charset.Charset, UTF-8 is
783  // available on every Java implementation.
784  Log.e(LOG_TAG, "UTF-8 is unsupported?", e);
785  return "";
786  }
787  }
788 
789 
797  public String UriDecode(String text) {
798  try {
799  return URLDecoder.decode(text, "UTF-8");
800  } catch (UnsupportedEncodingException e) {
801  // If UTF-8 is not supported, we're in big trouble!
802  // According to Javadoc and Android documentation for java.nio.charset.Charset, UTF-8 is
803  // available on every Java implementation.
804  Log.e(LOG_TAG, "UTF-8 is unsupported?", e);
805  return "";
806  }
807  }
808 
822  // This returns an object, which in general will be a Java ArrayList, String, Boolean, Integer,
823  // or Double.
824  // The object will be sanitized to produce the corresponding Yail data by call-component-method.
825  // That mechanism would need to be extended if we ever change JSON decoding to produce
826  // dictionaries rather than lists
827  // TOOD(hal): Provide an alternative way to decode JSON objects to dictionaries. Maybe with
828  // renaming this JsonTextDecodeWithPairs and making JsonTextDecode the one to use
829  // dictionaries
830  public Object JsonTextDecode(String jsonText) {
831  try {
832  return decodeJsonText(jsonText, false);
833  } catch (IllegalArgumentException e) {
834  form.dispatchErrorOccurredEvent(this, "JsonTextDecode",
836  return "";
837  }
838  }
839 
849  public Object JsonTextDecodeWithDictionaries(String jsonText) {
850  try {
851  return decodeJsonText(jsonText, true);
852  } catch (IllegalArgumentException e) {
853  form.dispatchErrorOccurredEvent(this, "JsonTextDecodeWithDictionaries",
855  return "";
856  }
857  }
858 
868  @VisibleForTesting
869  static Object decodeJsonText(String jsonText, boolean useDicts) throws IllegalArgumentException {
870  try {
871  return JsonUtil.getObjectFromJson(jsonText, useDicts);
872  } catch (JSONException e) {
873  throw new IllegalArgumentException("jsonText is not a legal JSON value");
874  }
875  }
876 
885  @Deprecated
886  @VisibleForTesting
887  static Object decodeJsonText(String jsonText) throws IllegalArgumentException {
888  return decodeJsonText(jsonText, false);
889  }
890 
900  @SimpleFunction
901  public String JsonObjectEncode(Object jsonObject) {
902  try {
903  return JsonUtil.encodeJsonObject(jsonObject);
904  } catch (IllegalArgumentException e) {
905  form.dispatchErrorOccurredEvent(this, "JsonObjectEncode",
907  return "";
908  }
909  }
910 
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) {
949  try {
950  XmlParser p = new XmlParser();
951  SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
952  InputSource is = new InputSource(new StringReader(XmlText));
953  is.setEncoding("UTF-8");
954  parser.parse(is, p);
955  return p.getRoot();
956  } catch (Exception e) {
957  Log.e(LOG_TAG, e.getMessage());
958  form.dispatchErrorOccurredEvent(this, "XMLTextDecodeAsDictionary",
960  return new YailDictionary();
961  }
962  }
963 
987  // This method works by by first converting the XML to JSON and then decoding the JSON.
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.")
990  // The above description string is punted because I can't figure out how to write the
991  // documentation string in a way that will look work both as a tooltip and in the autogenerated
992  // HTML for the component documentation on the Web. It's too long for a tooltip, anyway.
993  public Object XMLTextDecode(String XmlText) {
994  try {
995  return JsonTextDecode(XML.toJSONObject(XmlText).toString());
996  } catch (com.google.appinventor.components.runtime.repackaged.org.json.JSONException e) {
997  // We could be more precise and signal different errors for the conversion to JSON
998  // versus the decoding of that JSON, but showing the actual error message should
999  // be good enough.
1000  Log.e(LOG_TAG, e.getMessage());
1001  form.dispatchErrorOccurredEvent(this, "XMLTextDecode",
1003  // This XMLTextDecode should always return a list, even in the case of an error
1004  return YailList.makeEmptyList();
1005  }
1006  }
1007 
1018  @SimpleFunction(description = "Decodes the given HTML text value. HTML character entities " +
1019  "such as &amp;amp;, &amp;lt;, &amp;gt;, &amp;apos;, and &amp;quot; are changed to " +
1020  "&amp;, &lt;, &gt;, &#39;, and &quot;. Entities such as &amp;#xhhhh, and &amp;#nnnn " +
1021  "are changed to the appropriate characters.")
1022  public String HtmlTextDecode(String htmlText) {
1023  try {
1024  return HtmlEntities.decodeHtmlText(htmlText);
1025  } catch (IllegalArgumentException e) {
1026  form.dispatchErrorOccurredEvent(this, "HtmlTextDecode",
1028  return "";
1029  }
1030  }
1031 
1032  /*
1033  * Perform a HTTP GET or POST request.
1034  * This method is always run on a different thread than the event thread. It does not use any
1035  * property value fields because the properties may be changed while it is running. Instead, it
1036  * uses the parameters.
1037  * If either postData or postFile is non-null, then a post request is performed.
1038  * If both postData and postFile are non-null, postData takes precedence over postFile.
1039  * If postData and postFile are both null, then a get request is performed.
1040  * If saveResponse is true, the response will be saved in a file and the GotFile event will be
1041  * triggered. responseFileName specifies the name of the file.
1042  * If saveResponse is false, the GotText event will be triggered.
1043  *
1044  * This method can throw an IOException. The caller is responsible for catching it and
1045  * triggering the appropriate error event.
1046  *
1047  * @param webProps the captured property values needed for the request
1048  * @param postData the data for the post request if it is not coming from a file, can be null
1049  * @param postFile the path of the file containing data for the post request if it is coming from
1050  * a file, can be null
1051  *
1052  * @throws IOException
1053  */
1054  private void performRequest(final CapturedProperties webProps, final byte[] postData,
1055  final String postFile, final String httpVerb, final String method) {
1056 
1057  // Make sure we have permissions we may need
1058  if (saveResponse & !havePermission) {
1059  final Web me = this;
1060  form.askPermission(new BulkPermissionRequest(this, "Web",
1061  Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
1062  @Override
1063  public void onGranted() {
1064  me.havePermission = true;
1065  // onGranted is running on the UI thread, and we are about to do network i/o, so
1066  // we have to run this asynchronously to get off the UI thread!
1067  AsynchUtil.runAsynchronously(new Runnable() {
1068  @Override
1069  public void run() {
1070  me.performRequest(webProps, postData, postFile, httpVerb, method);
1071  }
1072  });
1073  }
1074  });
1075  return;
1076  }
1077 
1078  try {
1079  // Open the connection.
1080  HttpURLConnection connection = openConnection(webProps, httpVerb);
1081  if (connection != null) {
1082  try {
1083  if (postData != null) {
1084  writeRequestData(connection, postData);
1085  } else if (postFile != null) {
1086  writeRequestFile(connection, postFile);
1087  }
1088 
1089  // Get the response.
1090  final int responseCode = connection.getResponseCode();
1091  final String responseType = getResponseType(connection);
1092  processResponseCookies(connection);
1093 
1094  if (saveResponse) {
1095  final String path = saveResponseContent(connection, webProps.responseFileName,
1096  responseType);
1097 
1098  // Dispatch the event.
1099  activity.runOnUiThread(new Runnable() {
1100  @Override
1101  public void run() {
1102  GotFile(webProps.urlString, responseCode, responseType, path);
1103  }
1104  });
1105  } else {
1106  final String responseContent = getResponseContent(connection);
1107 
1108  // Dispatch the event.
1109  activity.runOnUiThread(new Runnable() {
1110  @Override
1111  public void run() {
1112  GotText(webProps.urlString, responseCode, responseType, responseContent);
1113  }
1114  });
1115  }
1116 
1117  } catch (SocketTimeoutException e) {
1118  // Dispatch timeout event.
1119  activity.runOnUiThread(new Runnable() {
1120  @Override
1121  public void run() {
1122  TimedOut(webProps.urlString);
1123  }
1124  });
1125  throw new RequestTimeoutException();
1126  } finally {
1127  connection.disconnect();
1128  }
1129  }
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) {
1139  int message;
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;
1148  } else {
1149  message = ErrorMessages.ERROR_WEB_UNABLE_TO_POST_OR_PUT;
1150  }
1151  form.dispatchErrorOccurredEvent(Web.this, method,
1152  message, webProps.urlString);
1153  }
1154  }
1155 
1166  private static HttpURLConnection openConnection(CapturedProperties webProps, String httpVerb)
1167  throws IOException, ClassCastException, ProtocolException {
1168 
1169  HttpURLConnection connection = (HttpURLConnection) webProps.url.openConnection();
1170  connection.setConnectTimeout(webProps.timeout);
1171  connection.setReadTimeout(webProps.timeout);
1172 
1173  if (httpVerb.equals("PUT") || httpVerb.equals("DELETE")){
1174  // Set the Request Method; GET is the default, and if it is a POST, it will be marked as such
1175  // with setDoOutput in writeRequestFile or writeRequestData
1176  connection.setRequestMethod(httpVerb);
1177  }
1178 
1179  // Request Headers
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);
1184  }
1185  }
1186 
1187  // Cookies
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);
1193  }
1194  }
1195  }
1196 
1197  return connection;
1198  }
1199 
1200  private static void writeRequestData(HttpURLConnection connection, byte[] postData)
1201  throws IOException {
1202  // According to the documentation at
1203  // http://developer.android.com/reference/java/net/HttpURLConnection.html
1204  // HttpURLConnection uses the GET method by default. It will use POST if setDoOutput(true) has
1205  // been called.
1206  connection.setDoOutput(true); // This makes it something other than a HTTP GET.
1207  // Write the data.
1208  connection.setFixedLengthStreamingMode(postData.length);
1209  BufferedOutputStream out = new BufferedOutputStream(connection.getOutputStream());
1210  try {
1211  out.write(postData, 0, postData.length);
1212  out.flush();
1213  } finally {
1214  out.close();
1215  }
1216  }
1217 
1218  private void writeRequestFile(HttpURLConnection connection, String path)
1219  throws IOException {
1220  // Use MediaUtil.openMedia to open the file. This means that path could be file on the SD card,
1221  // an asset, a contact picture, etc.
1222  BufferedInputStream in = new BufferedInputStream(MediaUtil.openMedia(form, path));
1223  try {
1224  // Write the file's data.
1225  // According to the documentation at
1226  // http://developer.android.com/reference/java/net/HttpURLConnection.html
1227  // HttpURLConnection uses the GET method by default. It will use POST if setDoOutput(true) has
1228  // been called.
1229  connection.setDoOutput(true); // This makes it something other than a HTTP GET.
1230  connection.setChunkedStreamingMode(0);
1231  BufferedOutputStream out = new BufferedOutputStream(connection.getOutputStream());
1232  try {
1233  while (true) {
1234  int b = in.read();
1235  if (b == -1) {
1236  break;
1237  }
1238  out.write(b);
1239  }
1240  out.flush();
1241  } finally {
1242  out.close();
1243  }
1244  } finally {
1245  in.close();
1246  }
1247  }
1248 
1249  private static String getResponseType(HttpURLConnection connection) {
1250  String responseType = connection.getContentType();
1251  return (responseType != null) ? responseType : "";
1252  }
1253 
1254  private void processResponseCookies(HttpURLConnection connection) {
1255  if (allowCookies && cookieHandler != null) {
1256  try {
1257  Map<String, List<String>> headerFields = connection.getHeaderFields();
1258  cookieHandler.put(connection.getURL().toURI(), headerFields);
1259  } catch (URISyntaxException e) {
1260  // Can't convert the URL to a URI; no cookies for you.
1261  } catch (IOException e) {
1262  // Sorry, no cookies for you.
1263  }
1264  }
1265  }
1266 
1267  private static String getResponseContent(HttpURLConnection connection) throws IOException {
1268  // Use the content encoding to convert bytes to characters.
1269  String encoding = connection.getContentEncoding();
1270  if (encoding == null) {
1271  encoding = "UTF-8";
1272  }
1273  InputStreamReader reader = new InputStreamReader(getConnectionStream(connection), encoding);
1274  try {
1275  int contentLength = connection.getContentLength();
1276  StringBuilder sb = (contentLength != -1)
1277  ? new StringBuilder(contentLength)
1278  : new StringBuilder();
1279  char[] buf = new char[1024];
1280  int read;
1281  while ((read = reader.read(buf)) != -1) {
1282  sb.append(buf, 0, read);
1283  }
1284  return sb.toString();
1285  } finally {
1286  reader.close();
1287  }
1288  }
1289 
1290  private String saveResponseContent(HttpURLConnection connection,
1291  String responseFileName, String responseType) throws IOException {
1292  File file = createFile(responseFileName, responseType);
1293 
1294  BufferedInputStream in = new BufferedInputStream(getConnectionStream(connection), 0x1000);
1295  try {
1296  BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file), 0x1000);
1297  try {
1298  // Copy the contents from the input stream to the output stream.
1299  while (true) {
1300  int b = in.read();
1301  if (b == -1) {
1302  break;
1303  }
1304  out.write(b);
1305  }
1306  out.flush();
1307  } finally {
1308  out.close();
1309  }
1310  } finally {
1311  in.close();
1312  }
1313 
1314  return file.getAbsolutePath();
1315  }
1316 
1317  private static InputStream getConnectionStream(HttpURLConnection connection) throws SocketTimeoutException {
1318  // According to the Android reference documentation for HttpURLConnection: If the HTTP response
1319  // indicates that an error occurred, getInputStream() will throw an IOException. Use
1320  // getErrorStream() to read the error response.
1321  try {
1322  return connection.getInputStream();
1323  } catch (SocketTimeoutException e) {
1324  throw e; //Rethrow exception - should not attempt to read stream for timeouts
1325  } catch (IOException e1) {
1326  // Use the error response for all other IO Exceptions.
1327  return connection.getErrorStream();
1328  }
1329  }
1330 
1331  private File createFile(String fileName, String responseType)
1332  throws IOException, FileUtil.FileException {
1333  // If a fileName was specified, use it.
1334  if (!TextUtils.isEmpty(fileName)) {
1335  return FileUtil.getExternalFile(form, fileName);
1336  }
1337 
1338  // Otherwise, try to determine an appropriate file extension from the responseType.
1339  // The response type could contain extra information that we don't need. For example, it might
1340  // be "text/html; charset=ISO-8859-1". We just want to look at the part before the semicolon.
1341  int indexOfSemicolon = responseType.indexOf(';');
1342  if (indexOfSemicolon != -1) {
1343  responseType = responseType.substring(0, indexOfSemicolon);
1344  }
1345  String extension = mimeTypeToExtension.get(responseType);
1346  if (extension == null) {
1347  extension = "tmp";
1348  }
1349  return FileUtil.getDownloadFile(form, extension);
1350  }
1351 
1352  /*
1353  * Converts request headers (a YailList) into the structure that can be used with the Java API
1354  * (a Map<String, List<String>>). If the request headers contains an invalid element, an
1355  * InvalidRequestHeadersException will be thrown.
1356  */
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);
1362  // Each item must be a two-element sublist.
1363  if (item instanceof YailList) {
1364  YailList sublist = (YailList) item;
1365  if (sublist.size() == 2) {
1366  // The first element is the request header field name.
1367  String fieldName = sublist.getObject(0).toString();
1368  // The second element contains the request header field values.
1369  Object fieldValues = sublist.getObject(1);
1370 
1371  // Build an entry (key and values) for the requestHeadersMap.
1372  String key = fieldName;
1373  List<String> values = Lists.newArrayList();
1374 
1375  // If there is just one field value, it is specified as a single non-list item (for
1376  // example, it can be a text value). If there are multiple field values, they are
1377  // specified as a list.
1378  if (fieldValues instanceof YailList) {
1379  // It's a list. There are multiple field values.
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());
1384  }
1385  } else {
1386  // It's a single non-list item. There is just one field value.
1387  Object singleFieldValue = fieldValues;
1388  values.add(singleFieldValue.toString());
1389  }
1390  // Put the entry into the requestHeadersMap.
1391  requestHeadersMap.put(key, values);
1392  } else {
1393  // The sublist doesn't contain two elements.
1394  throw new InvalidRequestHeadersException(
1395  ErrorMessages.ERROR_WEB_REQUEST_HEADER_NOT_TWO_ELEMENTS, i + 1);
1396  }
1397  } else {
1398  // The item isn't a sublist.
1399  throw new InvalidRequestHeadersException(
1400  ErrorMessages.ERROR_WEB_REQUEST_HEADER_NOT_LIST, i + 1);
1401  }
1402  }
1403  return requestHeadersMap;
1404  }
1405 
1406  /*
1407  * Captures the current property values that are needed for an HTTP request. If an error occurs
1408  * while validating the Url or RequestHeaders property values, this method calls
1409  * form.dispatchErrorOccurredEvent and returns null.
1410  *
1411  * @param functionName the name of the function, used when dispatching errors
1412  */
1413  private CapturedProperties capturePropertyValues(String functionName) {
1414  try {
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);
1421  }
1422  return null;
1423  }
1424 }
com.google.appinventor.components.runtime.EventDispatcher
Definition: EventDispatcher.java:22
com.google.appinventor.components.runtime.util.SdkLevel.LEVEL_GINGERBREAD
static final int LEVEL_GINGERBREAD
Definition: SdkLevel.java:26
com.google.appinventor.components.runtime.util.YailList
Definition: YailList.java:26
com.google.appinventor.components.annotations.SimpleFunction
Definition: SimpleFunction.java:23
com.google.appinventor.components.annotations.UsesLibraries
Definition: UsesLibraries.java:21
com.google.appinventor.components.runtime.util.ErrorMessages
Definition: ErrorMessages.java:17
com.google.appinventor.components.runtime.util
-*- mode: java; c-basic-offset: 2; -*-
Definition: AccountChooser.java:7
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_WEB_JSON_TEXT_DECODE_FAILED
static final int ERROR_WEB_JSON_TEXT_DECODE_FAILED
Definition: ErrorMessages.java:131
com.google.appinventor.components.runtime.util.FileUtil
Definition: FileUtil.java:37
com.google.appinventor.components.common.YaVersion
Definition: YaVersion.java:14
com.google.appinventor.components.annotations.DesignerProperty
Definition: DesignerProperty.java:25
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_STRING
static final String PROPERTY_TYPE_STRING
Definition: PropertyTypeConstants.java:237
com.google.appinventor.components
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER
static final String PROPERTY_TYPE_NON_NEGATIVE_INTEGER
Definition: PropertyTypeConstants.java:206
com.google.appinventor.components.runtime.util.XmlParser.getRoot
YailDictionary getRoot()
Definition: XmlParser.java:78
com.google.appinventor.components.runtime.util.JsonUtil
Definition: JsonUtil.java:42
com.google.appinventor.components.runtime.Web.RequestHeaders
void RequestHeaders(YailList list)
Definition: Web.java:288
com.google.appinventor.components.runtime.Web.UriEncode
String UriEncode(String text)
Definition: Web.java:777
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN
static final String PROPERTY_TYPE_BOOLEAN
Definition: PropertyTypeConstants.java:35
com.google.appinventor.components.runtime.util.MediaUtil
Definition: MediaUtil.java:53
com.google.appinventor.components.annotations.DesignerComponent
Definition: DesignerComponent.java:22
com.google.appinventor.components.annotations.SimpleEvent
Definition: SimpleEvent.java:20
com.google.appinventor.components.annotations.PropertyCategory.BEHAVIOR
BEHAVIOR
Definition: PropertyCategory.java:15
JSONException
com.google.appinventor.components.runtime.errors.IllegalArgumentError
Definition: IllegalArgumentError.java:17
com.google.appinventor.components.runtime.Web.AllowCookies
void AllowCookies(boolean allowCookies)
Definition: Web.java:318
com.google.appinventor.components.runtime.collect
Definition: Lists.java:7
com.google.appinventor.components.runtime.Web.UriDecode
String UriDecode(String text)
Definition: Web.java:797
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_FUNCTIONALITY_NOT_SUPPORTED_WEB_COOKIES
static final int ERROR_FUNCTIONALITY_NOT_SUPPORTED_WEB_COOKIES
Definition: ErrorMessages.java:23
com.google.appinventor.components.runtime.Web.Get
void Get()
Definition: Web.java:417
com.google.appinventor.components.annotations.UsesPermissions
Definition: UsesPermissions.java:21
com.google.appinventor.components.runtime.Web.GotFile
void GotFile(String url, int responseCode, String responseType, String fileName)
Definition: Web.java:702
com.google.appinventor.components.runtime.Web.Web
Web()
Definition: Web.java:239
com.google.appinventor.components.runtime.Web
Definition: Web.java:102
com.google.appinventor.components.runtime.Web.BuildRequestData
String BuildRequestData(YailList list)
Definition: Web.java:726
XML
com.google.appinventor.components.runtime.Web.Web
Web(ComponentContainer container)
Definition: Web.java:227
com.google.appinventor.components.runtime.EventDispatcher.dispatchEvent
static boolean dispatchEvent(Component component, String eventName, Object...args)
Definition: EventDispatcher.java:188
com.google.appinventor.components.runtime.util.GingerbreadUtil.newCookieManager
static CookieHandler newCookieManager()
Definition: GingerbreadUtil.java:43
com.google.appinventor.components.runtime.util.JsonUtil.encodeJsonObject
static String encodeJsonObject(Object jsonObject)
Definition: JsonUtil.java:493
com.google.appinventor.components.runtime.Web.JsonTextDecodeWithDictionaries
Object JsonTextDecodeWithDictionaries(String jsonText)
Definition: Web.java:849
com.google.appinventor.components.runtime.AndroidNonvisibleComponent
Definition: AndroidNonvisibleComponent.java:17
com.google.appinventor.components.runtime.util.SdkLevel
Definition: SdkLevel.java:19
com.google.appinventor.components.runtime.Web.JsonTextDecode
Object JsonTextDecode(String jsonText)
Definition: Web.java:830
com.google.appinventor.components.runtime.Web.Timeout
void Timeout(int timeout)
Definition: Web.java:388
com.google.appinventor.components.runtime.collect.Maps
Definition: Maps.java:19
com.google.appinventor.components.annotations.SimpleProperty
Definition: SimpleProperty.java:23
com.google.appinventor.components.runtime.Web.Url
void Url(String url)
Definition: Web.java:262
com.google.appinventor.components.runtime.util.BulkPermissionRequest
Definition: BulkPermissionRequest.java:22
com.google.appinventor.components.runtime.collect.Maps.newHashMap
static< K, V > HashMap< K, V > newHashMap()
Definition: Maps.java:25
com.google.appinventor.components.runtime.util.GingerbreadUtil
Definition: GingerbreadUtil.java:36
com.google.appinventor.components.runtime.util.AsynchUtil.runAsynchronously
static void runAsynchronously(final Runnable call)
Definition: AsynchUtil.java:23
com.google.appinventor.components.annotations.PropertyCategory
Definition: PropertyCategory.java:13
com.google.appinventor.components.runtime.Web.GotText
void GotText(String url, int responseCode, String responseType, String responseContent)
Definition: Web.java:687
com.google.appinventor.components.runtime.errors.PermissionException
Definition: PermissionException.java:16
com.google.appinventor.components.runtime.ComponentContainer
Definition: ComponentContainer.java:16
com.google.appinventor.components.common.HtmlEntities
Definition: HtmlEntities.java:22
com.google.appinventor.components.runtime.util.SdkLevel.getLevel
static int getLevel()
Definition: SdkLevel.java:45
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google.appinventor.components.runtime.util.GingerbreadUtil.clearCookies
static boolean clearCookies(CookieHandler cookieHandler)
Definition: GingerbreadUtil.java:54
com.google.appinventor.components.runtime.Component
Definition: Component.java:17
com.google.appinventor.components.runtime.Map
Definition: Map.java:84
com.google.appinventor.components.runtime.collect.Lists
Definition: Lists.java:20
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_WEB_JSON_TEXT_ENCODE_FAILED
static final int ERROR_WEB_JSON_TEXT_ENCODE_FAILED
Definition: ErrorMessages.java:142
com.google.appinventor.components.common
Definition: ComponentCategory.java:7
com.google.appinventor.components.runtime.Web.ResponseFileName
void ResponseFileName(String responseFileName)
Definition: Web.java:366
com.google.appinventor.components.common.ComponentCategory
Definition: ComponentCategory.java:48
com.google.appinventor.components.runtime.util.JsonUtil.getObjectFromJson
static Object getObjectFromJson(String jsonString)
Definition: JsonUtil.java:317
com.google.appinventor.components.annotations.SimpleObject
Definition: SimpleObject.java:23
com.google.appinventor.components.runtime.util.AsynchUtil
Definition: AsynchUtil.java:17
com.google
com
com.google.appinventor.components.common.HtmlEntities.decodeHtmlText
static String decodeHtmlText(String htmlText)
Definition: HtmlEntities.java:319
com.google.appinventor.components.runtime.util.YailDictionary
Definition: YailDictionary.java:32
com.google.appinventor.components.runtime.util.XmlParser
Definition: XmlParser.java:17
com.google.appinventor.components.runtime.errors
Definition: ArrayIndexOutOfBoundsError.java:7
com.google.appinventor.components.runtime.ComponentContainer.$form
Form $form()
com.google.appinventor.components.runtime.ComponentContainer.$context
Activity $context()
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_WEB_HTML_TEXT_DECODE_FAILED
static final int ERROR_WEB_HTML_TEXT_DECODE_FAILED
Definition: ErrorMessages.java:132
com.google.appinventor.components.runtime.util.YailList.makeEmptyList
static YailList makeEmptyList()
Definition: YailList.java:52
com.google.appinventor.components.runtime.util.YailList.getObject
Object getObject(int index)
Definition: YailList.java:200
com.google.appinventor.components.runtime.Web.JsonObjectEncode
String JsonObjectEncode(Object jsonObject)
Definition: Web.java:901
com.google.appinventor.components.runtime.Web.TimedOut
void TimedOut(String url)
Definition: Web.java:713
com.google.appinventor.components.runtime.Web.Delete
void Delete()
Definition: Web.java:614
com.google.appinventor.components.common.PropertyTypeConstants
Definition: PropertyTypeConstants.java:14
com.google.appinventor.components.annotations
com.google.appinventor.components.runtime.util.YailList.size
int size()
Definition: YailList.java:172
com.google.appinventor
com.google.appinventor.components.runtime.Web.SaveResponse
void SaveResponse(boolean saveResponse)
Definition: Web.java:341
com.google.appinventor.components.runtime.errors.RequestTimeoutException
Definition: RequestTimeoutException.java:15