// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.components.runtime; import com.google.appinventor.components.annotations.DesignerComponent; import com.google.appinventor.components.annotations.DesignerProperty; import com.google.appinventor.components.annotations.PropertyCategory; import com.google.appinventor.components.annotations.SimpleEvent; import com.google.appinventor.components.annotations.SimpleFunction; import com.google.appinventor.components.annotations.SimpleObject; import com.google.appinventor.components.annotations.SimpleProperty; import com.google.appinventor.components.annotations.UsesPermissions; import com.google.appinventor.components.common.ComponentCategory; import com.google.appinventor.components.common.PropertyTypeConstants; import com.google.appinventor.components.common.YaVersion; import com.google.appinventor.components.runtime.collect.Lists; import com.google.appinventor.components.runtime.util.AsyncCallbackPair; import com.google.appinventor.components.runtime.util.AsynchUtil; import com.google.appinventor.components.runtime.util.WebServiceUtil; import android.app.Activity; import android.os.Handler; import android.util.Log; import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; /** * The Voting component communicates with a Web service to retrieve a ballot * and send back users' votes. * * <p>The application should call the method <code>RequestBallot</code>, usually * in the <code>Initialize</code> event handler, in order to get the ballot * question and options from the Web service (specified by the * <code>ServiceURL</code> property). Depending on the response from the * Web service, the system will raise one of the following three events: * <ol> * <li> <code>GotBallot</code>, indicating that the ballot question and options * were retrieved and the properties <code>BallotQuestion</code> and * <code>BallotOptions</code> have been set.</li> * <li> <code>NoOpenPoll</code>, indicating that no ballot question is * available.</li> * <li> <code>WebServiceError</code>, indicating that the service did not * provide a legal response and providing an error messages.</li> * </ol></p> * * <p>After getting the ballot, the application should allow the user to make * a choice from among <code>BallotOptions</code> and set the property * <code>UserChoice</code> to that choice. The application should also set * <code>UserId</code> to specify which user is voting.</p> * * <p>Once the application has set <code>UserChoice</code> and * <code>UserId</code>, the application can call <code>SendBallot</code> to * send this information to the Web service. If the service successfully * receives the vote, the event <code>GotBallotConfirmation</code> will be * raised. Otherwise, the event <code>WebServiceError</code> will be raised * with the appropriate error message.</p> * * @author halabelson@google.com (Hal Abelson) */ @DesignerComponent(version = YaVersion.VOTING_COMPONENT_VERSION, designerHelpDescription = "<p>The Voting component enables users to vote " + "on a question by communicating with a Web service to retrieve a ballot " + "and later sending back users' votes.</p>", category = ComponentCategory.INTERNAL, // moved to Internal until fully tested nonVisible = true, iconName = "images/voting.png") @SimpleObject @UsesPermissions(permissionNames = "android.permission.INTERNET") public class Voting extends AndroidNonvisibleComponent implements Component { private static final String LOG_TAG = "Voting"; private static final String REQUESTBALLOT_COMMAND = "requestballot"; private static final String SENDBALLOT_COMMAND = "sendballot"; private static final String IS_POLLING_PARAMETER = "isPolling"; private static final String ID_REQUESTED_PARAMETER = "idRequested"; private static final String BALLOT_QUESTION_PARAMETER = "question"; private static final String BALLOT_OPTIONS_PARAMETER = "options"; private static final String USER_CHOICE_PARAMETER = "userchoice"; private static final String USER_ID_PARAMETER = "userid"; private Handler androidUIHandler; private ComponentContainer theContainer; private Activity activityContext; private String userId; private String serviceURL; private String ballotQuestion; private String ballotOptionsString; // The choices that a vote selects among private ArrayList<String> ballotOptions; // TODO(halabelson): idRequested isn't used in this version, but we'll keep it for the future private Boolean idRequested; private String userChoice; private Boolean isPolling; public Voting(ComponentContainer container){ super(container.$form()); serviceURL = "http://androvote.appspot.com"; userId = ""; isPolling = false; idRequested = false; ballotQuestion = ""; ballotOptions = new ArrayList<String>(); userChoice = ""; androidUIHandler = new Handler(); theContainer = container; activityContext = container.$context(); // We set the initial value of serviceURL to be the // demo Web service serviceURL = "http://androvote.appspot.com"; } /** * The URL of the Voting Service */ @SimpleProperty( description = "The URL of the Voting service", category = PropertyCategory.BEHAVIOR) public String ServiceURL() { return serviceURL; } /** * Set the URL of the Voting Service * * @param serviceURL the URL (includes initial http:, but no trailing slash) */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "http://androvote.appspot.com") @SimpleProperty public void ServiceURL(String serviceURL) { this.serviceURL = serviceURL; } /** * The question to be voted on. */ @SimpleProperty( description = "The question to be voted on.", category = PropertyCategory.BEHAVIOR) public String BallotQuestion() { return ballotQuestion; } /** * The list of choices to vote. */ @SimpleProperty( description = "The list of ballot options.", category = PropertyCategory.BEHAVIOR) public List<String> BallotOptions(){ return ballotOptions; } // This should not be settable by the user // @SimpleProperty // public void BallotOptions(String ballotOptions){ // this.ballotOptions = ballotOptions; // } /** * An Id that is sent to the Web server along with the vote. */ @SimpleProperty( description = "A text identifying the voter that is sent to the Voting " + "server along with the vote. This must be set before " + "<code>SendBallot</code> is called.", category = PropertyCategory.BEHAVIOR) public String UserId() { return userId; } /** * Set an Id to be sent to the Web server along with the vote. * * @param userId the string to use as the Id */ @SimpleProperty public void UserId(String userId){ this.userId = userId; } /** * The choice to select when sending the vote. */ @SimpleProperty( description = "The ballot choice to send to the server, which must be " + "set before <code>SendBallot</code> is called. " + "This must be one of <code>BallotOptions</code>.", category = PropertyCategory.BEHAVIOR) public String UserChoice() { return userChoice; } /** * Set the choice to select when sending the vote. * * @param userChoice the choice to select. Must be one of the BallotOptions */ @SimpleProperty public void UserChoice(String userChoice){ this.userChoice = userChoice; } /** * Returns the registered email address, as a string, for this * device's user. */ @SimpleProperty( description = "The email address associated with this device. This property has been " + "deprecated and always returns the empty text value.", category = PropertyCategory.BEHAVIOR) public String UserEmailAddress() { // The UserEmailAddress has not been supported since before the Gingerbread release, so we // suspect that nobody is relying on it, and are therefore deprecating it. If it happens that // it needs to be added back, the way to get an email address, is to force the user to select // an account. This has to be done asynchronously (not here on the UI thread), generally when // the application starts. However, that would mean that the application would always ask the // user to select an account at startup, even if the application never actually accesses this // property, which would possibly be alarming to the user of a Voting application. return ""; } /* RequestBallot will talk to the Web service and retrieve the ballot of * the current open poll. Depending on the service response, two events * might be triggered: NoOpenPoll or GotBallot. * When a ballot is received, the JSON response looks like this: * {"isPolling" : "true", * "idRequested" : "true", * "question" : "What are you?", * "options": [ "I'm a PC", "I'm a Mac" ] } */ /** * Send a request ballot command to the Voting server. */ @SimpleFunction( description = "Send a request for a ballot to the Web service specified " + "by the property <code>ServiceURL</code>. When the " + "completes, one of the following events will be raised: " + "<code>GotBallot</code>, <code>NoOpenPoll</code>, or " + "<code>WebServiceError</code>.") public void RequestBallot() { final Runnable call = new Runnable() { public void run() { postRequestBallot(); }}; AsynchUtil.runAsynchronously(call); } private void postRequestBallot(){ AsyncCallbackPair<JSONObject> myCallback = new AsyncCallbackPair<JSONObject>() { public void onSuccess(JSONObject result) { if (result == null) { // Signal a Web error event to indicate that there was no response // to this request for a ballot. androidUIHandler.post(new Runnable() { public void run() { WebServiceError("The Web server did not respond to your request for a ballot"); } }); return; } else { try { Log.i(LOG_TAG, "postRequestBallot: ballot retrieved " + result); // The Web service is designed to return the JSON encoded object // This has to be a legal JSON encoding. For example, true and false // should not be quoted if we're using getBoolean. A bad encoding will // throw a JSON exception. isPolling = result.getBoolean(IS_POLLING_PARAMETER); if (isPolling){ //populate parameter's value directly from reading JSONObject idRequested = result.getBoolean(ID_REQUESTED_PARAMETER); ballotQuestion = result.getString(BALLOT_QUESTION_PARAMETER); ballotOptionsString = result.getString(BALLOT_OPTIONS_PARAMETER); ballotOptions = JSONArrayToArrayList(new JSONArray(ballotOptionsString)); androidUIHandler.post(new Runnable() { public void run() { GotBallot(); } }); } else { androidUIHandler.post(new Runnable() { public void run() { NoOpenPoll(); } }); } } catch (JSONException e) { // Signal a Web error event to indicate the the server // returned a garbled value. From the user's perspective, // there may be no practical difference between this and // the "no response" error above, but application writers // can create handlers to use these events as they choose. // Note that server errors that create malformed JSON // responses will sometimes be caught here. androidUIHandler.post(new Runnable() { public void run() { WebServiceError("The Web server returned a garbled object"); } }); return; } } } public void onFailure(final String message) { Log.w(LOG_TAG, "postRequestBallot Failure " + message); androidUIHandler.post(new Runnable() { public void run() { WebServiceError(message); } }); return; } }; WebServiceUtil.getInstance().postCommandReturningObject( serviceURL, REQUESTBALLOT_COMMAND, null, myCallback); return; } private ArrayList<String> JSONArrayToArrayList(JSONArray ja) throws JSONException { ArrayList<String> a = new ArrayList<String>(); for (int i = 0; i < ja.length(); i++) { a.add(ja.getString(i)); } return a; } /** * Event indicating that a ballot was received from the Web service. */ @SimpleEvent( description = "Event indicating that a ballot was retrieved from the Web " + "service and that the properties <code>BallotQuestion</code> and " + "<code>BallotOptions</code> have been set. This is always preceded " + "by a call to the method <code>RequestBallot</code>.") public void GotBallot() { EventDispatcher.dispatchEvent(this, "GotBallot"); } /** * Event indicating that the service has no open poll. */ @SimpleEvent public void NoOpenPoll() { EventDispatcher.dispatchEvent(this, "NoOpenPoll"); } /** * Send a ballot to the Web Voting server. The userId and the choice are * specified by the UserId and UserChoice properties. */ @SimpleFunction( description = "Send a completed ballot to the Web service. This should " + "not be called until the properties <code>UserId</code> " + "and <code>UserChoice</code> have been set by the application.") public void SendBallot() { final Runnable call = new Runnable() { public void run() { postSendBallot(userChoice, userId); }}; AsynchUtil.runAsynchronously(call); } private void postSendBallot(String userChoice, String userId){ AsyncCallbackPair<String> myCallback = new AsyncCallbackPair<String>(){ // the Web service will send back a confirmation message, but // the component ignores it and notes only that anything at // all was sent back. We can improve this later. public void onSuccess(String response) { androidUIHandler.post(new Runnable() { public void run() { GotBallotConfirmation(); } }); } public void onFailure(final String message) { Log.w(LOG_TAG, "postSendBallot Failure " + message); androidUIHandler.post(new Runnable() { public void run() { WebServiceError(message); } }); return; } }; WebServiceUtil.getInstance().postCommand(serviceURL, SENDBALLOT_COMMAND, Lists.<NameValuePair>newArrayList( new BasicNameValuePair(USER_CHOICE_PARAMETER, userChoice), new BasicNameValuePair(USER_ID_PARAMETER, userId)), myCallback); } /** * Event confirming that the Voting service received the ballot. */ @SimpleEvent public void GotBallotConfirmation() { EventDispatcher.dispatchEvent(this, "GotBallotConfirmation"); } //----------------------------------------------------------------------------- /** * Event indicating that the communication with the Web service resulted in * an error. * * @param message the error message */ @SimpleEvent public void WebServiceError(String message) { // Invoke the application's "WebServiceError" event handler EventDispatcher.dispatchEvent(this, "WebServiceError", message); } }