6 package com.google.appinventor.components.runtime;
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;
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;
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;
56 import java.io.BufferedReader;
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;
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> 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> " +
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>" +
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,
128 iconName =
"images/fusiontables.png")
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," +
150 private static final String LOG_TAG =
"FUSION";
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";
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 ";
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;
167 private String authTokenType = AUTH_TOKEN_TYPE_FUSIONTABLES;
173 private String apiKey;
179 private String query;
184 private String queryResultStr;
199 private String standardErrorMessage =
"Error on Fusion Tables query";
202 private String errorMessage;
205 private final Activity activity;
212 private String keyPath =
"";
217 private boolean isServiceAuth =
false;
222 private String serviceAccountEmail =
"";
224 private String scope =
"https://www.googleapis.com/auth/fusiontables";
226 private String loadingDialogMessage =
"Please wait loading...";
228 private boolean showLoadingDialog =
true;
231 super(componentContainer.
$form());
232 this.container = componentContainer;
233 this.activity = componentContainer.
$context();
234 requestHelper = createClientLoginHelper(DIALOG_TEXT, FUSIONTABLE_SERVICE);
235 query = DEFAULT_QUERY;
239 "Sorry. The Fusiontables component is not compatible with this phone.",
240 "This application must exit.",
258 private void showNoticeAndDie(String message, String title, String buttonText){
259 AlertDialog alertDialog =
new AlertDialog.Builder(activity).create();
260 alertDialog.setTitle(title);
262 alertDialog.setCancelable(
false);
263 alertDialog.setMessage(message);
264 alertDialog.setButton(buttonText,
new DialogInterface.OnClickListener() {
265 public void onClick(DialogInterface dialog,
int which) {
275 @SimpleProperty(category = PropertyCategory.BEHAVIOR,
276 description =
"Indicates whether a service account should be used for authentication")
277 public
boolean UseServiceAuthentication() {
278 return isServiceAuth;
284 this.isServiceAuth = bool;
291 description =
"The Service Account Email Address when service account authentication " +
293 public String ServiceAccountEmail() {
294 return serviceAccountEmail;
300 this.serviceAccountEmail = email;
311 this.apiKey = apiKey;
319 description =
"Your Google API Key. For help, click on the question" +
320 "mark (?) next to the FusiontablesControl component in the Palette. ",
322 public String ApiKey() {
327 defaultValue = DEFAULT_QUERY)
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> ",
342 public String Query() {
352 if (path.equals(keyPath)) {
357 if (cachedServiceCredentials !=
null) {
358 cachedServiceCredentials.delete();
359 cachedServiceCredentials =
null;
361 keyPath = (path ==
null) ?
"" : path;
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() {
376 @
SimpleFunction(description =
"Send the query to the Fusiontables server.")
377 public
void SendQuery() {
378 new QueryProcessorV2(activity).execute(query);
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);
390 form.dispatchErrorOccurredEvent(
this,
"DoQuery",
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) {
408 description =
"Forget end-users login credentials. Has no effect on service authentication")
409 public
void ForgetLogin() {
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);
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);
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);
445 defaultValue =
"Please wait loading...")
448 this.loadingDialogMessage = loadingDialogMessage;
456 description =
"Set the loading message for the dialog.",
458 public String LoadingDialogMessage() {
459 return loadingDialogMessage;
466 defaultValue =
"True")
469 this.showLoadingDialog = showLoadingDialog;
477 description =
"Whether or not to show the loading dialog",
479 public
boolean ShowLoadingDialog() {
480 return showLoadingDialog;
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);
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);
514 private class QueryProcessor
extends AsyncTask<String, Void, String> {
515 private ProgressDialog progress =
null;
518 protected void onPreExecute() {
519 progress = ProgressDialog.show(activity,
"Fusiontables",
"processing query...",
true);
527 protected String doInBackground(String... params) {
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) {
538 return e.getMessage();
547 protected void onPostExecute(String result) {
573 errorMessage = standardErrorMessage;
574 Log.i(LOG_TAG,
"executing " + query);
575 com.
google.api.client.http.HttpResponse response =
null;
578 Fusiontables service =
new Fusiontables.Builder(
579 AndroidHttp.newCompatibleTransport(),
581 new GoogleCredential())
582 .setApplicationName(
"App Inventor Fusiontables/v2.0")
583 .setJsonHttpRequestInitializer(
new GoogleKeyInitializer(ApiKey()))
590 ((Fusiontables) service).query().sql(query);
591 sql.put(
"alt",
"csv");
594 sql.setOauthToken(authToken);
596 response = sql.executeUnparsed();
598 }
catch (GoogleJsonResponseException e) {
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) {
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);
621 String resultStr =
"";
622 if (response !=
null) {
623 if (response.getStatusCode() != 200) {
624 resultStr = response.getStatusCode() +
" " + response.getStatusMessage();
627 resultStr = parseResponse(response.getContent());
628 }
catch (IOException e) {
644 String resultStr =
"";
645 if (response !=
null) {
646 if (response.getStatusLine().getStatusCode() != 200) {
647 resultStr = response.getStatusLine().getStatusCode() +
" "
648 + response.getStatusLine().getReasonPhrase();
651 resultStr = parseResponse(response.getEntity().getContent());
652 }
catch (IOException e) {
666 String resultStr =
"";
668 BufferedReader br =
new BufferedReader(
new InputStreamReader(input));
670 StringBuilder sb =
new StringBuilder();
673 while ((line = br.readLine()) !=
null) {
674 sb.append(line +
"\n");
676 resultStr = sb.toString();
677 Log.i(LOG_TAG,
"resultStr = " + resultStr);
679 }
catch (IOException e) {
691 Log.i(LOG_TAG,
"handleOAuthError: " + msg);
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(
",");
723 jsonContent.append(
"],");
724 jsonContent.append(
"'isExportable':'true',");
725 jsonContent.append(
"'name': '" + tableName +
"'");
726 jsonContent.append(
"}");
728 jsonContent.insert(0,
"CREATE TABLE ");
730 Log.i(LOG_TAG,
"result = " + jsonContent.toString());
731 return jsonContent.toString();
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);
751 StringEntity entity =
null;
752 HttpPost request =
new HttpPost(FUSIONTABLES_POST +
"?key=" + ApiKey());
754 entity =
new StringEntity(jsonContent);
755 }
catch (UnsupportedEncodingException e) {
757 return "Error: " + e.getMessage();
759 entity.setContentType(
"application/json");
760 request.addHeader(
"Authorization", AUTHORIZATION_HEADER_PREFIX + authToken);
761 request.setEntity(entity);
765 HttpClient client =
new DefaultHttpClient();
767 response = client.execute(request);
768 }
catch (ClientProtocolException e) {
770 return "Error: " + e.getMessage();
771 }
catch (IOException e) {
773 return "Error: " + e.getMessage();
779 int statusCode = response.getStatusLine().getStatusCode();
780 if (response !=
null && statusCode == 200) {
782 String jsonResult = FusiontablesControl.httpApacheResponseToString(response);
783 JSONObject jsonObj =
new JSONObject(jsonResult);
784 if (jsonObj.has(
"tableId")) {
785 queryResultStr =
"tableId," + jsonObj.get(
"tableId");
787 queryResultStr = jsonResult;
790 }
catch (IllegalStateException e) {
792 return "Error: " + e.getMessage();
793 }
catch (JSONException e) {
795 return "Error: " + e.getMessage();
797 Log.i(LOG_TAG,
"Response code = " + response.getStatusLine());
798 Log.i(LOG_TAG,
"Query = " + query +
"\nResultStr = " + queryResultStr);
801 Log.i(LOG_TAG,
"Error: " + response.getStatusLine().toString());
802 queryResultStr = response.getStatusLine().toString();
805 return queryResultStr;
815 private class QueryProcessorV2
extends AsyncTask<String, Void, String> {
816 private static final String TAG =
"QueryProcessorV2";
819 private static final String STAG =
"FUSION_SERVICE_ACCOUNT";
821 private final Activity activity;
822 private final ProgressDialog dialog;
827 QueryProcessorV2(Activity activity) {
828 Log.i(TAG,
"Creating AsyncFusiontablesQuery");
829 this.activity = activity;
830 dialog =
new ProgressDialog(activity);
834 protected void onPreExecute() {
835 if (ShowLoadingDialog()) {
836 dialog.setMessage(LoadingDialogMessage());
845 protected String doInBackground(String... params) {
846 String query = params[0];
847 Log.i(TAG,
"Starting doInBackground " + query);
849 return serviceAuthRequest(query);
851 return userAuthRequest(query);
855 private String userAuthRequest(String query) {
859 OAuth2Helper oauthHelper =
new OAuth2Helper();
860 String authToken = oauthHelper.getRefreshedAuthToken(activity, authTokenType);
864 if (authToken !=
null) {
867 if (query.toLowerCase().contains(
"create table")) {
868 queryResultStr = doPostRequest(parseSqlCreateQueryToJson(query), authToken);
869 return queryResultStr;
873 com.
google.api.client.http.HttpResponse response = sendQuery(query, authToken);
876 if (response !=
null) {
877 queryResultStr = httpResponseToString(response);
878 Log.i(TAG,
"Query = " + query +
"\nResultStr = " + queryResultStr);
880 queryResultStr = errorMessage;
881 Log.i(TAG,
"Error: " + errorMessage);
883 return queryResultStr;
886 return OAuth2Helper.getErrorMessage();
890 private String serviceAuthRequest(String query) {
893 errorMessage = standardErrorMessage;
895 final HttpTransport TRANSPORT = AndroidHttp.newCompatibleTransport();
896 final JsonFactory JSON_FACTORY =
new GsonFactory();
898 Log.i(STAG,
"keyPath " + keyPath);
901 if (cachedServiceCredentials ==
null) {
905 cachedServiceCredentials = MediaUtil.copyMediaToTempFile(container.
$form(), keyPath);
907 GoogleCredential credential =
new GoogleCredential.Builder()
908 .setTransport(TRANSPORT)
909 .setJsonFactory(JSON_FACTORY)
910 .setServiceAccountId(serviceAccountEmail)
911 .setServiceAccountScopes(scope)
912 .setServiceAccountPrivateKeyFromP12File(cachedServiceCredentials)
915 Fusiontables fusiontables =
new Fusiontables.Builder(TRANSPORT, JSON_FACTORY, credential)
916 .setJsonHttpRequestInitializer(
new GoogleKeyInitializer(ApiKey()))
920 Sql sql = fusiontables.query().sql(query);
921 sql.put(
"alt",
"csv");
923 com.
google.api.client.http.HttpResponse response =
null;
929 response = sql.executeUnparsed();
931 }
catch (GoogleJsonResponseException e) {
935 Log.i(STAG,
"Got a JsonResponse exception on sql.executeUnparsed");
940 errorMessage = parseJsonResponseException(e.getMessage());
941 signalJsonResponseError(query, errorMessage);
943 }
catch (Exception e) {
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");
954 errorMessage = e.getMessage();
955 signalJsonResponseError(query, errorMessage);
960 if (response !=
null) {
962 queryResultStr = httpResponseToString(response);
963 Log.i(STAG,
"Query = " + query +
"\nResultStr = " + queryResultStr);
968 queryResultStr = errorMessage;
969 Log.i(STAG,
"Error with null response: " + errorMessage);
972 Log.i(STAG,
"executed sql query");
974 }
catch (Throwable e) {
975 Log.i(STAG,
"in Catch Throwable e");
977 queryResultStr = e.getMessage();
980 Log.i(STAG,
"returning queryResultStr = " + queryResultStr);
981 return queryResultStr;
985 String parseJsonResponseException(String exceptionMessage) {
986 Log.i(STAG,
"parseJsonResponseException: " + exceptionMessage);
990 return exceptionMessage;
998 protected void onPostExecute(String result) {
999 Log.i(LOG_TAG,
"Query result " + result);
1000 if (result ==
null) {
1001 result = errorMessage;
1008 void signalJsonResponseError(String query, String parsedException) {
1014 form.dispatchErrorOccurredEventDialog(
this,
"SendQuery",
1015 ErrorMessages.FUSION_TABLES_QUERY_ERROR, query, parsedException);