AI2 Component  (Version nb184)
FusiontablesControl.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-2019 MIT, All rights reserved
4 // Released under the Apache License, Version 2.0
5 // http://www.apache.org/licenses/LICENSE-2.0
6 package com.google.appinventor.components.runtime;
7 
8 import com.google.api.client.extensions.android2.AndroidHttp;
9 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
10 import com.google.api.client.googleapis.json.GoogleJsonResponseException;
11 import com.google.api.client.googleapis.services.GoogleKeyInitializer;
12 import com.google.api.client.json.JsonFactory;
13 import com.google.api.client.json.gson.GsonFactory;
14 import com.google.api.client.http.HttpTransport;
15 import com.google.api.services.fusiontables.Fusiontables;
16 import com.google.api.services.fusiontables.Fusiontables.Query.Sql;
35 
36 import android.app.Activity;
37 import android.app.AlertDialog;
38 import android.app.ProgressDialog;
39 import android.content.DialogInterface;
40 import android.os.AsyncTask;
41 import android.util.Log;
42 
43 import org.apache.http.HttpResponse;
44 import org.apache.http.client.ClientProtocolException;
45 import org.apache.http.client.HttpClient;
46 import org.apache.http.client.entity.UrlEncodedFormEntity;
47 import org.apache.http.client.methods.HttpPost;
48 import org.apache.http.client.methods.HttpUriRequest;
49 import org.apache.http.entity.StringEntity;
50 import org.apache.http.impl.client.DefaultHttpClient;
51 import org.apache.http.message.BasicNameValuePair;
52 import org.apache.http.params.HttpConnectionParams;
53 import org.json.JSONException;
54 import org.json.JSONObject;
55 
56 import java.io.BufferedReader;
57 import java.io.File;
58 import java.io.ByteArrayOutputStream;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.InputStreamReader;
62 import java.io.UnsupportedEncodingException;
63 import java.util.ArrayList;
64 
87 @DesignerComponent(version = YaVersion.FUSIONTABLESCONTROL_COMPONENT_VERSION,
88  description = "<p>A non-visible component that communicates with Google Fusion Tables. " +
89  "Fusion Tables let you store, share, query and visualize data tables; " +
90  "this component lets you query, create, and modify these tables.</p> " +
91  "<p><font color=red><b>NOTE:</b>&nbsp;Google shutdown the Fusion Tables service on December 3, 2019. This " +
92  "component no longer functions.</font></p> " +
93  "<p>This component uses the " +
94  "<a href=\"https://developers.google.com/fusiontables/docs/v2/getting_started\" target=\"_blank\">Fusion Tables API V2.0</a>. " +
95  "<p>Applications using Fusion Tables must authentication to Google's servers. There " +
96  "are two ways this can be done. The first way uses an API Key which you the developer " +
97  "obtain (see below). With this approach end-users must also login to access a Fusion Table. " +
98  "The second approach is to use a Service Account. With this approach you create credentials " +
99  "and a special \"Service Account Email Address\" which you obtain from the " +
100  "<a href=\"https://code.google.com/apis/console/\" target=\"_blank\">Google APIs Console</a>. " +
101  "You then tell the Fusion Table Control the name of the Service Account Email address and upload " +
102  "the secret key as an asset to your application and set the KeyFile property to point at this " +
103  "file. Finally you check the \"UseServiceAuthentication\" checkbox in the designer. " +
104  "When using a Service Account, end-users do not need to login to use Fusion Tables, " +
105  "your service account authenticates all access.</p> " +
106  "<p>To get an API key, follow these instructions.</p> " +
107  "<ol>" +
108  "<li>Go to your <a href=\"https://code.google.com/apis/console/\" target=\"_blank\">Google APIs Console</a> and login if necessary.</li>" +
109  "<li>Select the <i>Services</i> item from the menu on the left.</li>" +
110  "<li>Choose the <i>Fusiontables</i> service from the list provided and turn it on.</li>" +
111  "<li>Go back to the main menu and select the <i>API Access</i> item. </li>" +
112  "</ol>" +
113  "<p>Your API Key will be near the bottom of that pane in the section called \"Simple API Access\"." +
114  "You will have to provide that key as the value for the <i>ApiKey</i> property in your Fusiontables app.</p>" +
115  "<p>Once you have an API key, set the value of the <i>Query</i> property to a valid Fusiontables SQL query " +
116  "and call <i>SendQuery</i> to execute the query. App Inventor will send the query to the Fusion Tables " +
117  "server and the <i>GotResult</i> block will fire when a result is returned from the server." +
118  "Query results will be returned in CSV format, and " +
119  "can be converted to list format using the \"list from csv table\" or " +
120  "\"list from csv row\" blocks.</p>" +
121  "<p>Note that you do not need to worry about UTF-encoding the query. " +
122  "But you do need to make sure the query follows the syntax described in " +
123  "<a href=\"https://developers.google.com/fusiontables/docs/v2/getting_started\" target=\"_blank\">the reference manual</a>, " +
124  "which means that things like capitalization for names of columns matters, and " +
125  "that single quotes must be used around column names if there are spaces in them.</p>",
126  category = ComponentCategory.INTERNAL,
127  nonVisible = true,
128  iconName = "images/fusiontables.png")
129 @SimpleObject
130 @UsesPermissions(permissionNames =
131  "android.permission.INTERNET," +
132  "android.permission.ACCOUNT_MANAGER," +
133  "android.permission.MANAGE_ACCOUNTS," +
134  "android.permission.GET_ACCOUNTS," +
135  "android.permission.USE_CREDENTIALS," +
136  "android.permission.WRITE_EXTERNAL_STORAGE," +
137  "android.permission.READ_EXTERNAL_STORAGE")
138 @UsesLibraries(libraries =
139  "fusiontables.jar," +
140  "google-api-client-beta.jar," +
141  "google-api-client-android2-beta.jar," +
142  "google-http-client-beta.jar," +
143  "google-http-client-android2-beta.jar," +
144  "google-http-client-android3-beta.jar," +
145  "google-oauth-client-beta.jar," +
146  "guava-14.0.1.jar," +
147  "gson-2.1.jar")
148 
150  private static final String LOG_TAG = "FUSION";
151 
152 
153 
154  private static final String DIALOG_TEXT = "Choose an account to access FusionTables";
155  private static final String FUSION_QUERY_URL = "http://www.google.com/fusiontables/v2/query";
156  public static final String FUSIONTABLES_POST = "https://www.googleapis.com/fusiontables/v2/tables";
157 
158  private static final String DEFAULT_QUERY = "show tables";
159  private static final String FUSIONTABLE_SERVICE = "fusiontables";
160  private static final int SERVER_TIMEOUT_MS = 30000;
161  public static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
162 
163  public static final String AUTH_TOKEN_TYPE_FUSIONTABLES = "oauth2:https://www.googleapis.com/auth/fusiontables";
164  public static final String APP_NAME = "App Inventor";
165  private File cachedServiceCredentials = null; // if using service accounts, temp location of credentials.
166 
167  private String authTokenType = AUTH_TOKEN_TYPE_FUSIONTABLES;
168 
173  private String apiKey;
174 
175 
179  private String query;
180 
184  private String queryResultStr;
185 
192  // standard error message to return
193  // private String standardErrorMessage = Ode.MESSAGES.FusionTablesStandardErrorMessage();
194  // TODO(hal): Internationalize this correctly. I don't know how to use
195  // the entries in Ode.MESSAGES since this is not a method, event, or property
196  // Do we need to add another category of words to be localized, or can use
197  // use the mechanism that's already there?
198 
199  private String standardErrorMessage = "Error on Fusion Tables query";
200 
201  // variable to hold error message (which might be computed from an exception)
202  private String errorMessage;
203 
204 
205  private final Activity activity;
206  private final ComponentContainer container;
207  private final IClientLoginHelper requestHelper;
208 
212  private String keyPath = "";
213 
217  private boolean isServiceAuth = false;
218 
222  private String serviceAccountEmail = "";
223 
224  private String scope = "https://www.googleapis.com/auth/fusiontables";
225 
226  private String loadingDialogMessage = "Please wait loading...";
227 
228  private boolean showLoadingDialog = true;
229 
230  public FusiontablesControl(ComponentContainer componentContainer) {
231  super(componentContainer.$form());
232  this.container = componentContainer;
233  this.activity = componentContainer.$context();
234  requestHelper = createClientLoginHelper(DIALOG_TEXT, FUSIONTABLE_SERVICE);
235  query = DEFAULT_QUERY;
236 
238  showNoticeAndDie(
239  "Sorry. The Fusiontables component is not compatible with this phone.",
240  "This application must exit.",
241  "Rats!");
242  }
243 
244  // comment: The above code was originally
245  // Toast.makeText(activity,
246  // "Sorry. The Fusiontables component is not compatible with your phone. Exiting.",
247  // Toast.LENGTH_LONG).show();
248  // activity.finish();
249  // I'm leaving this here for the edification of future developers. The code does not work
250  // because Toasts do not block: The activity will finish immediately, regardless of
251  // the length of the toast, and the message will not be readable. The new version isn't
252  // quite right either because the app will execute Screen.Initialize while the message
253  // is being shown.
254 
255  }
256 
257  // show a notification and kill the app when the button is pressed
258  private void showNoticeAndDie(String message, String title, String buttonText){
259  AlertDialog alertDialog = new AlertDialog.Builder(activity).create();
260  alertDialog.setTitle(title);
261  // prevents the user from escaping the dialog by hitting the Back button
262  alertDialog.setCancelable(false);
263  alertDialog.setMessage(message);
264  alertDialog.setButton(buttonText, new DialogInterface.OnClickListener() {
265  public void onClick(DialogInterface dialog, int which) {
266  activity.finish();
267  }});
268  alertDialog.show();
269  }
270 
275  @SimpleProperty(category = PropertyCategory.BEHAVIOR,
276  description = "Indicates whether a service account should be used for authentication")
277  public boolean UseServiceAuthentication() {
278  return isServiceAuth;
279  }
280 
281  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False")
283  public void UseServiceAuthentication(boolean bool) {
284  this.isServiceAuth = bool;
285  }
286 
291  description = "The Service Account Email Address when service account authentication " +
292  "is in use.")
293  public String ServiceAccountEmail() {
294  return serviceAccountEmail;
295  }
296 
297  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
299  public void ServiceAccountEmail(String email) {
300  this.serviceAccountEmail = email;
301  }
302 
303 
308  defaultValue = "")
310  public void ApiKey(String apiKey) {
311  this.apiKey = apiKey;
312  }
313 
319  description = "Your Google API Key. For help, click on the question" +
320  "mark (?) next to the FusiontablesControl component in the Palette. ",
321  category = PropertyCategory.BEHAVIOR)
322  public String ApiKey() {
323  return apiKey;
324  }
325 
327  defaultValue = DEFAULT_QUERY)
329  public void Query(String query) {
330  this.query = query;
331  }
332 
334  description = "The query to send to the Fusion Tables API. " +
335  "<p>For legal query formats and examples, see the " +
336  "<a href=\"https://developers.google.com/fusiontables/docs/v2/getting_started\" target=\"_blank\">Fusion Tables API v2.0 reference manual</a>.</p> " +
337  "<p>Note that you do not need to worry about UTF-encoding the query. " +
338  "But you do need to make sure it follows the syntax described in the reference manual, " +
339  "which means that things like capitalization for names of columns matters, " +
340  "and that single quotes need to be used around column names if there are spaces in them.</p> ",
341  category = PropertyCategory.BEHAVIOR)
342  public String Query() {
343  return query;
344  }
345 
347  defaultValue = "")
349  public void KeyFile(String path) {
350  // If it's the same as on the prior call and the prior load was successful,
351  // do nothing.
352  if (path.equals(keyPath)) {
353  return;
354  }
355 
356  // Remove old cached credentials if we are changing the keyPath
357  if (cachedServiceCredentials != null) {
358  cachedServiceCredentials.delete();
359  cachedServiceCredentials = null;
360  }
361  keyPath = (path == null) ? "" : path;
362  }
363 
365  category = PropertyCategory.BEHAVIOR,
366  description = "Specifies the path of the private key file. " +
367  "This key file is used to get access to the FusionTables API.")
368  public String KeyFile() {
369  return keyPath;
370  }
371 
376  @SimpleFunction(description = "Send the query to the Fusiontables server.")
377  public void SendQuery() {
378  new QueryProcessorV2(activity).execute(query);
379  }
380 
381  //Deprecated -- Won't work after 12/2012
382  @Deprecated // [lyn, 2015/12/30] In AI2, now use explicit @Deprecated annotation rather than
383  // userVisible = false to deprecate an event, method, or property.
385  description = "DEPRECATED. This block is deprecated as of the end of 2012. Use SendQuery.")
386  public void DoQuery() {
387  if (requestHelper != null) {
388  new QueryProcessor().execute(query);
389  } else {
390  form.dispatchErrorOccurredEvent(this, "DoQuery",
392  }
393  }
394 
395  @SimpleEvent(
396  description = "Indicates that the Fusion Tables query has finished processing, " +
397  "with a result. The result of the query will generally be returned in CSV format, and " +
398  "can be converted to list format using the \"list from csv table\" or " +
399  "\"list from csv row\" blocks.")
400  public void GotResult(String result) {
401  // Invoke the application's "GotValue" event handler
402  EventDispatcher.dispatchEvent(this, "GotResult", result);
403  }
404 
405  // TODO(sharon): figure out why this isn't working
406  // TODO(ralph): Looks like it's working for OAuth 2, Let's user switch accounts
408  description = "Forget end-users login credentials. Has no effect on service authentication")
409  public void ForgetLogin() {
411  }
412 
414  description="Inserts a row into the specified fusion table. The tableId field is the id of the" +
415  "fusion table. The columns is a comma-separated list of the columns to insert values into. The" +
416  " values field specifies what values to insert into each column.")
417  public void InsertRow(String tableId, String columns, String values) {
418  query = "INSERT INTO " + tableId + " (" + columns + ")" + " VALUES " + "(" + values + ")";
419  new QueryProcessorV2(activity).execute(query);
420  }
421 
422 
424  description="Gets all the rows from a specified fusion table. The tableId field is the id of the" +
425  "required fusion table. The columns field is a comma-separeted list of the columns to retrieve.")
426  public void GetRows(String tableId, String columns) {
427  query = "SELECT " + columns + " FROM " + tableId;
428  new QueryProcessorV2(activity).execute(query);
429  }
430 
432  description="Gets all the rows from a fusion table that meet certain conditions. The tableId field is" +
433  "the id of the required fusion table. The columns field is a comma-separeted list of the columns to" +
434  "retrieve. The conditions field specifies what rows to retrieve from the table, for example the rows in which" +
435  "a particular column value is not null.")
436  public void GetRowsWithConditions(String tableId, String columns, String conditions) {
437  query = "SELECT " + columns + " FROM " + tableId + " WHERE " + conditions;
438  new QueryProcessorV2(activity).execute(query);
439  }
440 
445  defaultValue = "Please wait loading...")
447  public void LoadingDialogMessage(String loadingDialogMessage) {
448  this.loadingDialogMessage = loadingDialogMessage;
449  }
450 
456  description = "Set the loading message for the dialog.",
457  category = PropertyCategory.BEHAVIOR)
458  public String LoadingDialogMessage() {
459  return loadingDialogMessage;
460  }
461 
466  defaultValue = "True")
468  public void ShowLoadingDialog(boolean showLoadingDialog) {
469  this.showLoadingDialog = showLoadingDialog;
470  }
471 
477  description = "Whether or not to show the loading dialog",
478  category = PropertyCategory.BEHAVIOR)
479  public boolean ShowLoadingDialog() {
480  return showLoadingDialog;
481  }
482 
483 
484  // To be Deprecated, based on the old API
485  private IClientLoginHelper createClientLoginHelper(String accountPrompt, String service) {
487  HttpClient httpClient = new DefaultHttpClient();
488  HttpConnectionParams.setSoTimeout(httpClient.getParams(), SERVER_TIMEOUT_MS);
489  HttpConnectionParams.setConnectionTimeout(httpClient.getParams(), SERVER_TIMEOUT_MS);
490  return new ClientLoginHelper(activity, service, accountPrompt, httpClient);
491  }
492  return null;
493  }
494 
498  // To be deprecated, based on the old API
499  private HttpUriRequest genFusiontablesQuery(String query) throws IOException {
500  HttpPost request = new HttpPost(FUSION_QUERY_URL);
501  ArrayList<BasicNameValuePair> pair = new ArrayList<BasicNameValuePair>(1);
502  pair.add(new BasicNameValuePair("sql", query));
503  UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pair, "UTF-8");
504  entity.setContentType("application/x-www-form-urlencoded");
505  request.setEntity(entity);
506  return request;
507  }
508 
514  private class QueryProcessor extends AsyncTask<String, Void, String> {
515  private ProgressDialog progress = null;
516 
517  @Override
518  protected void onPreExecute() {
519  progress = ProgressDialog.show(activity, "Fusiontables", "processing query...", true);
520  }
521 
526  @Override
527  protected String doInBackground(String... params) {
528  try {
529  HttpUriRequest request = genFusiontablesQuery(params[0]);
530  Log.d(LOG_TAG, "Fetching: " + params[0]);
531  HttpResponse response = requestHelper.execute(request);
532  ByteArrayOutputStream outstream = new ByteArrayOutputStream();
533  response.getEntity().writeTo(outstream);
534  Log.d(LOG_TAG, "Response: " + response.getStatusLine().toString());
535  return outstream.toString();
536  } catch (IOException e) {
537  e.printStackTrace();
538  return e.getMessage();
539  }
540  }
541 
546  @Override
547  protected void onPostExecute(String result) {
548  progress.dismiss();
549  GotResult(result);
550  }
551  }
552 
572  public com.google.api.client.http.HttpResponse sendQuery(String query, String authToken) {
573  errorMessage = standardErrorMessage; // In case we get an error without a message
574  Log.i(LOG_TAG, "executing " + query);
575  com.google.api.client.http.HttpResponse response = null;
576 
577  // Create a Fusiontables service object (from Google API client lib)
578  Fusiontables service = new Fusiontables.Builder(
579  AndroidHttp.newCompatibleTransport(),
580  new GsonFactory(),
581  new GoogleCredential())
582  .setApplicationName("App Inventor Fusiontables/v2.0")
583  .setJsonHttpRequestInitializer(new GoogleKeyInitializer(ApiKey()))
584  .build();
585 
586  try {
587 
588  // Construct the SQL query and get a CSV result
589  Sql sql =
590  ((Fusiontables) service).query().sql(query);
591  sql.put("alt", "csv");
592 
593  // Add the authToken to authentication header
594  sql.setOauthToken(authToken);
595 
596  response = sql.executeUnparsed();
597 
598  } catch (GoogleJsonResponseException e) {
599  e.printStackTrace();
600  errorMessage = e.getMessage();
601  Log.e(LOG_TAG, "JsonResponseException");
602  Log.e(LOG_TAG, "e.getMessage() is " + e.getMessage());
603  Log.e(LOG_TAG, "response is " + response);
604  } catch (IOException e) {
605  e.printStackTrace();
606  errorMessage = e.getMessage();
607  Log.e(LOG_TAG, "IOException");
608  Log.e(LOG_TAG, "e.getMessage() is " + e.getMessage());
609  Log.e(LOG_TAG, "response is " + response);
610  }
611  return response;
612  }
613 
620  public static String httpResponseToString(com.google.api.client.http.HttpResponse response) {
621  String resultStr = "";
622  if (response != null) {
623  if (response.getStatusCode() != 200) {
624  resultStr = response.getStatusCode() + " " + response.getStatusMessage();
625  } else {
626  try {
627  resultStr = parseResponse(response.getContent());
628  } catch (IOException e) {
629  // TODO Auto-generated catch block
630  e.printStackTrace();
631  }
632  }
633  }
634  return resultStr;
635  }
636 
643  public static String httpApacheResponseToString(org.apache.http.HttpResponse response) {
644  String resultStr = "";
645  if (response != null) {
646  if (response.getStatusLine().getStatusCode() != 200) {
647  resultStr = response.getStatusLine().getStatusCode() + " "
648  + response.getStatusLine().getReasonPhrase();
649  } else {
650  try {
651  resultStr = parseResponse(response.getEntity().getContent());
652  } catch (IOException e) {
653  e.printStackTrace();
654  }
655  }
656  }
657  return resultStr;
658  }
659 
665  public static String parseResponse(InputStream input) {
666  String resultStr = "";
667  try {
668  BufferedReader br = new BufferedReader(new InputStreamReader(input));
669 
670  StringBuilder sb = new StringBuilder();
671 
672  String line;
673  while ((line = br.readLine()) != null) {
674  sb.append(line + "\n");
675  }
676  resultStr = sb.toString();
677  Log.i(LOG_TAG, "resultStr = " + resultStr);
678  br.close();
679  } catch (IOException e) {
680  e.printStackTrace();
681  }
682  return resultStr;
683  }
684 
685 
690  public void handleOAuthError(String msg) {
691  Log.i(LOG_TAG, "handleOAuthError: " + msg);
692  errorMessage = msg;
693  }
694 
708  private String parseSqlCreateQueryToJson (String query) {
709  Log.i(LOG_TAG, "parsetoJSonSqlCreate :" + query);
710  StringBuilder jsonContent = new StringBuilder();
711  query = query.trim();
712  String tableName = query.substring("create table".length(), query.indexOf('(')).trim();
713  String columnsList = query.substring(query.indexOf('(') + 1, query.indexOf(')'));
714  String [] columnSpecs = columnsList.split(",");
715  jsonContent.append("{'columns':[");
716  for (int k = 0; k < columnSpecs.length; k++) {
717  String [] nameTypePair = columnSpecs[k].split(":");
718  jsonContent.append("{'name': '" + nameTypePair[0].trim() + "', 'type': '" + nameTypePair[1].trim() + "'}" );
719  if (k < columnSpecs.length -1) {
720  jsonContent.append(",");
721  }
722  }
723  jsonContent.append("],");
724  jsonContent.append("'isExportable':'true',");
725  jsonContent.append("'name': '" + tableName + "'");
726  jsonContent.append("}");
727 
728  jsonContent.insert(0, "CREATE TABLE ");
729 
730  Log.i(LOG_TAG, "result = " + jsonContent.toString());
731  return jsonContent.toString();
732  }
733 
744  private String doPostRequest(String query, String authToken) {
745  org.apache.http.HttpResponse response = null;
746  String jsonContent = query.trim().substring("create table".length());
747  Log.i(LOG_TAG, "Http Post content = " + jsonContent);
748 
749  // Set up the POST request
750 
751  StringEntity entity = null;
752  HttpPost request = new HttpPost(FUSIONTABLES_POST + "?key=" + ApiKey()); // Fusiontables Uri
753  try {
754  entity = new StringEntity(jsonContent);
755  } catch (UnsupportedEncodingException e) {
756  e.printStackTrace();
757  return "Error: " + e.getMessage();
758  }
759  entity.setContentType("application/json");
760  request.addHeader("Authorization", AUTHORIZATION_HEADER_PREFIX + authToken);
761  request.setEntity(entity);
762 
763  // Execute the request
764 
765  HttpClient client = new DefaultHttpClient();
766  try {
767  response = client.execute(request);
768  } catch (ClientProtocolException e) {
769  e.printStackTrace();
770  return "Error: " + e.getMessage();
771  } catch (IOException e) {
772  e.printStackTrace();
773  return "Error: " + e.getMessage();
774  }
775 
776  // Process the response
777  // A valid response will have code=200 and contain a tableId value plus other stuff.
778  // We just return the table id.
779  int statusCode = response.getStatusLine().getStatusCode();
780  if (response != null && statusCode == 200) {
781  try {
782  String jsonResult = FusiontablesControl.httpApacheResponseToString(response);
783  JSONObject jsonObj = new JSONObject(jsonResult);
784  if (jsonObj.has("tableId")) {
785  queryResultStr = "tableId," + jsonObj.get("tableId");
786  } else {
787  queryResultStr = jsonResult;
788  }
789 
790  } catch (IllegalStateException e) {
791  e.printStackTrace();
792  return "Error: " + e.getMessage();
793  } catch (JSONException e) {
794  e.printStackTrace();
795  return "Error: " + e.getMessage();
796  }
797  Log.i(LOG_TAG, "Response code = " + response.getStatusLine());
798  Log.i(LOG_TAG, "Query = " + query + "\nResultStr = " + queryResultStr);
799  // queryResultStr = response.getStatusLine().toString();
800  } else {
801  Log.i(LOG_TAG, "Error: " + response.getStatusLine().toString());
802  queryResultStr = response.getStatusLine().toString();
803  }
804 
805  return queryResultStr;
806  }
807 
808 
815  private class QueryProcessorV2 extends AsyncTask<String, Void, String> {
816  private static final String TAG = "QueryProcessorV2";
817 
818  // alternative log tab used in service account processing
819  private static final String STAG = "FUSION_SERVICE_ACCOUNT";
820 
821  private final Activity activity; // The main list activity
822  private final ProgressDialog dialog;
823 
827  QueryProcessorV2(Activity activity) {
828  Log.i(TAG, "Creating AsyncFusiontablesQuery");
829  this.activity = activity;
830  dialog = new ProgressDialog(activity);
831  }
832 
833  @Override
834  protected void onPreExecute() {
835  if (ShowLoadingDialog()) {
836  dialog.setMessage(LoadingDialogMessage());
837  dialog.show();
838  }
839  }
840 
844  @Override
845  protected String doInBackground(String... params) {
846  String query = params[0];
847  Log.i(TAG, "Starting doInBackground " + query);
848  if (isServiceAuth) {
849  return serviceAuthRequest(query);
850  } else {
851  return userAuthRequest(query);
852  }
853  }
854 
855  private String userAuthRequest(String query) {
856  queryResultStr = "";
857 
858  // Get a fresh access token
859  OAuth2Helper oauthHelper = new OAuth2Helper();
860  String authToken = oauthHelper.getRefreshedAuthToken(activity, authTokenType);
861 
862  // Make the fusiontables query
863 
864  if (authToken != null) {
865 
866  // We handle CREATE TABLE as a special case
867  if (query.toLowerCase().contains("create table")) {
868  queryResultStr = doPostRequest(parseSqlCreateQueryToJson(query), authToken);
869  return queryResultStr;
870  } else {
871 
872  // Execute all other queries
873  com.google.api.client.http.HttpResponse response = sendQuery(query, authToken);
874 
875  // Process the response
876  if (response != null) {
877  queryResultStr = httpResponseToString(response);
878  Log.i(TAG, "Query = " + query + "\nResultStr = " + queryResultStr);
879  } else {
880  queryResultStr = errorMessage;
881  Log.i(TAG, "Error: " + errorMessage);
882  }
883  return queryResultStr;
884  }
885  } else {
886  return OAuth2Helper.getErrorMessage();
887  }
888  }
889 
890  private String serviceAuthRequest(String query) {
891 
892  queryResultStr = "";
893  errorMessage = standardErrorMessage;
894 
895  final HttpTransport TRANSPORT = AndroidHttp.newCompatibleTransport();
896  final JsonFactory JSON_FACTORY = new GsonFactory();
897 
898  Log.i(STAG, "keyPath " + keyPath);
899 
900  try {
901  if (cachedServiceCredentials == null) { // Need to cache the credentials in a temp file
902  // copyMediaToTempFile will copy the credentials either from the /sdcard if
903  // we are running in the Companion, or from the packaged assets if we are a
904  // packaged application.
905  cachedServiceCredentials = MediaUtil.copyMediaToTempFile(container.$form(), keyPath);
906  }
907  GoogleCredential credential = new GoogleCredential.Builder()
908  .setTransport(TRANSPORT)
909  .setJsonFactory(JSON_FACTORY)
910  .setServiceAccountId(serviceAccountEmail)
911  .setServiceAccountScopes(scope)
912  .setServiceAccountPrivateKeyFromP12File(cachedServiceCredentials)
913  .build();
914 
915  Fusiontables fusiontables = new Fusiontables.Builder(TRANSPORT, JSON_FACTORY, credential)
916  .setJsonHttpRequestInitializer(new GoogleKeyInitializer(ApiKey()))
917  .build();
918 
919  // See the try/catch below for the exception thrown if the query is bad SQL
920  Sql sql = fusiontables.query().sql(query);
921  sql.put("alt", "csv");
922 
923  com.google.api.client.http.HttpResponse response = null;
924 
925  try {
926  // if an error is thrown here, the catch clauses take care of signaling a form error
927  // to the end user, and the response will be null. The null response will cause
928  // the FusionTables.query command to return a standard error message as it result.
929  response = sql.executeUnparsed();
930 
931  } catch (GoogleJsonResponseException e) {
932  // This is the exception that was thrown as a result of a bad query to fusion tables.
933  // I determined this experimentally since I could not find documentation, so I don't know
934  // if throwing this particular exception is officially supported.
935  Log.i(STAG, "Got a JsonResponse exception on sql.executeUnparsed");
936 
937  // TODO(hal): In principle, we would parse the exception message to show a good user message.
938  // But for now parseJsonResponseException is a stub that returns the raw message
939  // Make the parser more intelligent
940  errorMessage = parseJsonResponseException(e.getMessage());
941  signalJsonResponseError(query, errorMessage);
942 
943  } catch (Exception e) {
944  // Maybe there could be some other kind of exception thrown?
945  Log.i(STAG, "Got an unanticipated exception on sql.executeUnparsed");
946  Log.i(STAG, "Exception class is " + e.getClass());
947  Log.i(STAG, "Exception message is " + e.getMessage());
948  Log.i(STAG, "Exception is " + e);
949  Log.i(STAG, "Point e");
950  Log.i(STAG, "end of printing exception"); // e might have been multiline
951 
952  // In the case of an unknown exception, we just show the user the exception message.
953  // If we knew the type of exception, we might be able to do something more useful
954  errorMessage = e.getMessage();
955  signalJsonResponseError(query, errorMessage);
956 
957  }
958 
959  // Process the response
960  if (response != null) {
961  // in the non-error case, get the response as a string to so we can return it
962  queryResultStr = httpResponseToString(response);
963  Log.i(STAG, "Query = " + query + "\nResultStr = " + queryResultStr);
964  } else {
965  // the response will be null if sql.executeUnparsed threw an error. In that
966  // case, the catch took care of signaling a form error to the user, and make
967  // the FusionTablesControl.query method return a standard error message.
968  queryResultStr = errorMessage;
969  Log.i(STAG, "Error with null response: " + errorMessage);
970  }
971 
972  Log.i(STAG, "executed sql query");
973 
974  } catch (Throwable e) {
975  Log.i(STAG, "in Catch Throwable e");
976  e.printStackTrace();
977  queryResultStr = e.getMessage();
978  }
979 
980  Log.i(STAG, "returning queryResultStr = " + queryResultStr);
981  return queryResultStr;
982  } //end of ServiceAuthRequest
983 
984 
985  String parseJsonResponseException(String exceptionMessage) {
986  Log.i(STAG, "parseJsonResponseException: " + exceptionMessage);
987  // This procedure is here as a stub in case we want to someday make the
988  // exception handling create better error messages for users. For
989  // now, we just return the raw message.
990  return exceptionMessage;
991  }
992 
993 
997  @Override
998  protected void onPostExecute(String result) {
999  Log.i(LOG_TAG, "Query result " + result);
1000  if (result == null) {
1001  result = errorMessage;
1002  }
1003  dialog.dismiss();
1004  GotResult(result);
1005  }
1006  }
1007 
1008  void signalJsonResponseError(String query, String parsedException) {
1009  // This will show the user the bad query, together with the resulting
1010  // exception.
1011  // We use dispatchErrorOccurredEventDialog because the message will be too long
1012  // to read as an alert. The app designer can override this with the Screen.ErrorOccurred
1013  // event, just as with ordinary dispatchErrorOccurred
1014  form.dispatchErrorOccurredEventDialog(this, "SendQuery",
1015  ErrorMessages.FUSION_TABLES_QUERY_ERROR, query, parsedException);
1016  }
1017 
1018 }
com.google.appinventor.components.runtime.EventDispatcher
Definition: EventDispatcher.java:22
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.FusiontablesControl.ShowLoadingDialog
void ShowLoadingDialog(boolean showLoadingDialog)
Definition: FusiontablesControl.java:468
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.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.runtime.util.IClientLoginHelper.execute
HttpResponse execute(HttpUriRequest request)
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN
static final String PROPERTY_TYPE_BOOLEAN
Definition: PropertyTypeConstants.java:35
com.google.appinventor.components.runtime.FusiontablesControl.KeyFile
void KeyFile(String path)
Definition: FusiontablesControl.java:349
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
com.google.appinventor.components.runtime.util.SdkLevel.LEVEL_ECLAIR
static final int LEVEL_ECLAIR
Definition: SdkLevel.java:22
com.google.appinventor.components.runtime.File
Definition: File.java:53
com.google.appinventor.components.runtime.util.OAuth2Helper
Definition: OAuth2Helper.java:71
com.google.appinventor.components.runtime.util.IClientLoginHelper
Definition: IClientLoginHelper.java:23
com.google.appinventor.components.runtime.FusiontablesControl.ApiKey
void ApiKey(String apiKey)
Definition: FusiontablesControl.java:310
com.google.appinventor.components.annotations.UsesPermissions
Definition: UsesPermissions.java:21
com.google.appinventor.components.runtime.FusiontablesControl.sendQuery
com.google.api.client.http.HttpResponse sendQuery(String query, String authToken)
Definition: FusiontablesControl.java:572
com.google.appinventor.components.runtime.FusiontablesControl.Query
void Query(String query)
Definition: FusiontablesControl.java:329
com.google.appinventor.components.runtime.FusiontablesControl.UseServiceAuthentication
void UseServiceAuthentication(boolean bool)
Definition: FusiontablesControl.java:283
com.google.appinventor.components.runtime.FusiontablesControl.httpResponseToString
static String httpResponseToString(com.google.api.client.http.HttpResponse response)
Definition: FusiontablesControl.java:620
com.google.appinventor.components.runtime.FusiontablesControl.httpApacheResponseToString
static String httpApacheResponseToString(org.apache.http.HttpResponse response)
Definition: FusiontablesControl.java:643
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.OAuth2Helper.resetAccountCredential
static void resetAccountCredential(Activity activity)
Definition: OAuth2Helper.java:211
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.annotations.SimpleProperty
Definition: SimpleProperty.java:23
com.google.appinventor.components.runtime.FusiontablesControl.handleOAuthError
void handleOAuthError(String msg)
Definition: FusiontablesControl.java:690
com.google.appinventor.components.annotations.PropertyCategory
Definition: PropertyCategory.java:13
com.google.appinventor.components.runtime.ComponentContainer
Definition: ComponentContainer.java:16
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.Component
Definition: Component.java:17
com.google.appinventor.components.runtime.FusiontablesControl.LoadingDialogMessage
void LoadingDialogMessage(String loadingDialogMessage)
Definition: FusiontablesControl.java:447
com.google.appinventor.components.common
Definition: ComponentCategory.java:7
com.google.appinventor.components.common.ComponentCategory
Definition: ComponentCategory.java:48
com.google.appinventor.components.runtime.FusiontablesControl.FusiontablesControl
FusiontablesControl(ComponentContainer componentContainer)
Definition: FusiontablesControl.java:230
com.google.appinventor.components.annotations.SimpleObject
Definition: SimpleObject.java:23
com.google
com.google.appinventor.components.runtime.FusiontablesControl
Definition: FusiontablesControl.java:149
com
com.google.appinventor.components.runtime.ComponentContainer.$form
Form $form()
com.google.appinventor.components.runtime.ComponentContainer.$context
Activity $context()
com.google.appinventor.components.runtime.FusiontablesControl.ServiceAccountEmail
void ServiceAccountEmail(String email)
Definition: FusiontablesControl.java:299
com.google.appinventor.components.runtime.util.ClientLoginHelper
Definition: ClientLoginHelper.java:38
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_ASSET
static final String PROPERTY_TYPE_ASSET
Definition: PropertyTypeConstants.java:22
com.google.appinventor.components.runtime.FusiontablesControl.parseResponse
static String parseResponse(InputStream input)
Definition: FusiontablesControl.java:665
com.google.appinventor.components.common.PropertyTypeConstants
Definition: PropertyTypeConstants.java:14
com.google.appinventor.components.annotations
com.google.appinventor
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_FUNCTIONALITY_NOT_SUPPORTED_FUSIONTABLES_CONTROL
static final int ERROR_FUNCTIONALITY_NOT_SUPPORTED_FUSIONTABLES_CONTROL
Definition: ErrorMessages.java:22