// -*- 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.errors.YailRuntimeError;
import com.google.appinventor.components.runtime.util.AsyncCallbackPair;
import com.google.appinventor.components.runtime.util.AsynchUtil;
import com.google.appinventor.components.runtime.util.GameInstance;
import com.google.appinventor.components.runtime.util.JsonUtil;
import com.google.appinventor.components.runtime.util.PlayerListDelta;
import com.google.appinventor.components.runtime.util.WebServiceUtil;
import com.google.appinventor.components.runtime.util.YailList;
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;
/**
* GameClient provides a way for AppInventor applications to
* communicate with online game servers. This allows users to create
* games that are coordinated and managed in the cloud.
*
* Most communication is done by sending keyed messages back and
* forth between the client and the server in the form of YailLists.
* The server and game client can then switch on the keys and perform
* more complex operations on the data. In addition, game servers can
* implement a library of server commands that can perform complex
* functions on the server and send back responses that are converted
* into YailLists and sent back to the component. For more
* information about server commands, consult the game server code
* at http://code.google.com/p/app-inventor-for-android/
*
* Games instances are uniquely determined by a game id and an
* instance id. In general, each App Inventor program should have
* its own game id. Then, when running different instances of that
* program, new instance ides should be used. Players are
* represented uniquely by the email address registered to their
* phones.
*
* All call functions perform POSTs to a web server. Upon successful
* completion of these POST requests, FunctionCompleted will be
* triggered with the function name as an argument. If the post
* fails, WebServiceError will trigger with the function name and the
* error message as arguments. These calls allow for application
* creators to deal with web service failures and keep track of the
* success or failure of their operations. The only exception to this
* is when the return value from the server has the incorrect game id
* or instance id. In this case, the response is completely ignored
* and neither of these events will trigger.
*
*
*/
@DesignerComponent(version = YaVersion.GAMECLIENT_COMPONENT_VERSION,
description = "Provides a way for applications to communicate with online game servers",
category = ComponentCategory.INTERNAL, // moved to internal until fully tested
nonVisible = true,
iconName = "images/gameClient.png")
@SimpleObject
@UsesPermissions(
permissionNames = "android.permission.INTERNET, " +
"com.google.android.googleapps.permission.GOOGLE_AUTH")
public class GameClient extends AndroidNonvisibleComponent
implements Component, OnResumeListener, OnStopListener {
private static final String LOG_TAG = "GameClient";
// Parameter keys
private static final String GAME_ID_KEY = "gid";
private static final String INSTANCE_ID_KEY = "iid";
private static final String PLAYER_ID_KEY = "pid";
private static final String INVITEE_KEY = "inv";
private static final String LEADER_KEY = "leader";
private static final String COUNT_KEY = "count";
private static final String TYPE_KEY = "type";
private static final String INSTANCE_PUBLIC_KEY = "makepublic";
private static final String MESSAGE_RECIPIENTS_KEY = "mrec";
private static final String MESSAGE_CONTENT_KEY = "contents";
private static final String MESSAGE_TIME_KEY = "mtime";
private static final String MESSAGE_SENDER_KEY = "msender";
private static final String COMMAND_TYPE_KEY = "command";
private static final String COMMAND_ARGUMENTS_KEY = "args";
private static final String SERVER_RETURN_VALUE_KEY = "response";
private static final String MESSAGES_LIST_KEY = "messages";
private static final String ERROR_RESPONSE_KEY = "e";
private static final String PUBLIC_LIST_KEY = "public";
private static final String JOINED_LIST_KEY = "joined";
private static final String INVITED_LIST_KEY = "invited";
private static final String PLAYERS_LIST_KEY = "players";
// Command keys
private static final String GET_INSTANCE_LISTS_COMMAND = "getinstancelists";
private static final String GET_MESSAGES_COMMAND = "messages";
private static final String INVITE_COMMAND = "invite";
private static final String JOIN_INSTANCE_COMMAND = "joininstance";
private static final String LEAVE_INSTANCE_COMMAND = "leaveinstance";
private static final String NEW_INSTANCE_COMMAND = "newinstance";
private static final String NEW_MESSAGE_COMMAND = "newmessage";
private static final String SERVER_COMMAND = "servercommand";
private static final String SET_LEADER_COMMAND = "setleader";
// URL for accessing the game server
private String serviceUrl;
private String gameId;
private GameInstance instance;
private Handler androidUIHandler;
private Activity activityContext;
private String userEmailAddress = "";
// Game instances in the current GameId that this player has joined
private List<String> joinedInstances;
// Game instances to which this player has been invited
private List<String> invitedInstances;
// Game instances which have been made public.
private List<String> publicInstances;
/**
* Creates a new GameClient component.
*
* @param container the Form that this component is contained in.
*/
public GameClient(ComponentContainer container) {
super(container.$form());
// Note that although this is creating a new Handler there is
// only one UI thread in an Android app and posting to this
// handler queues up a Runnable for execution on that thread.
androidUIHandler = new Handler();
activityContext = container.$context();
form.registerForOnResume(this);
form.registerForOnStop(this);
gameId = "";
instance = new GameInstance("");
joinedInstances = Lists.newArrayList();
invitedInstances = Lists.newArrayList();
publicInstances = Lists.newArrayList();
serviceUrl = "http://appinvgameserver.appspot.com";
// This needs to be done in a separate thread since it uses
// a blocking service to complete and will cause the UI to hang
// if it happens in the constructor.
/*
* Remove this code until we fix LoginServiceUtil to work in later
* versions of the android SDK.
AsynchUtil.runAsynchronously(new Runnable() {
@Override
public void run() {
userEmailAddress = LoginServiceUtil.getPhoneEmailAddress(activityContext);
if (!userEmailAddress.equals("")) {
UserEmailAddressSet(userEmailAddress);
}
}
});
*/
}
//----------------------------------------------------------------
// Properties
/**
* Returns a string indicating the game name for this application.
* The same game ID can have one or more game instances.
*/
@SimpleProperty(
description = "The game name for this application. " +
"The same game ID can have one or more game instances.",
category = PropertyCategory.BEHAVIOR)
public String GameId() {
return gameId;
}
/**
* Specifies a string indicating the family of the current game
* instance. The same game ID can have one or more game instance
* IDs.
*/
// Only exposed in the designer to enforce that each GameClient
// instance should be made for a single GameId.
@DesignerProperty(
editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING,
defaultValue = "")
public void GameId(String id) {
this.gameId = id;
}
/**
* Returns the game instance id. Taken together, the game ID and
* the instance ID uniquely identify the game.
*/
@SimpleProperty(
description = "The game instance id. Taken together," +
"the game ID and the instance ID uniquely identify the game.",
category = PropertyCategory.BEHAVIOR)
public String InstanceId() {
return instance.getInstanceId();
}
/**
* Returns the set of game instances to which this player has been
* invited but has not yet joined. To ensure current values are
* returned, first invoke {@link #GetInstanceLists}.
*/
@SimpleProperty(
description = "The set of game instances to which this player has been " +
"invited but has not yet joined. To ensure current values are " +
"returned, first invoke GetInstanceLists.",
category = PropertyCategory.BEHAVIOR)
public List<String> InvitedInstances() {
return invitedInstances;
}
/**
* Returns the set of game instances in which this player is
* participating. To ensure current values are returned, first
* invoke {@link #GetInstanceLists}.
*/
@SimpleProperty(
description = "The set of game instances in which this player is " +
"participating. To ensure current values are returned, first " +
"invoke GetInstanceLists.",
category = PropertyCategory.BEHAVIOR)
public List<String> JoinedInstances() {
return joinedInstances;
}
/**
* Returns the game's leader. At any time, each game instance has
* only one leader, but the leader may change with time.
* Initially, the leader is the game instance creator. Application
* writers determine special properties of the leader. The leader
* value is updated each time a successful communication is made
* with the server.
*/
@SimpleProperty(
description = "The game's leader. At any time, each game instance has " +
"only one leader, but the leader may change with time. " +
"Initially, the leader is the game instance creator. Application " +
"writers determine special properties of the leader. The leader " +
"value is updated each time a successful communication is made " +
"with the server.",
category = PropertyCategory.BEHAVIOR)
public String Leader() {
return instance.getLeader();
}
/**
* Returns the current set of players for this game instance. Each
* player is designated by an email address, which is a string. The
* list of players is updated each time a successful communication
* is made with the game server.
*/
@SimpleProperty(
description = "The current set of players for this game instance. Each " +
"player is designated by an email address, which is a string. The " +
"list of players is updated each time a successful communication " +
"is made with the game server.",
category = PropertyCategory.BEHAVIOR)
public List<String> Players() {
return instance.getPlayers();
}
/**
* Returns the set of game instances that have been marked public.
* To ensure current values are returned, first
* invoke {@link #GetInstanceLists}.
*/
@SimpleProperty(
description = "The set of game instances that have been marked public. " +
"To ensure current values are returned, first " +
"invoke {@link #GetInstanceLists}. ",
category = PropertyCategory.BEHAVIOR)
public List<String> PublicInstances() {
return publicInstances;
}
/**
* The URL of the game server.
*/
@SimpleProperty(
description = "The URL of the game server.",
category = PropertyCategory.BEHAVIOR)
public String ServiceUrl() {
return serviceUrl;
}
/**
* Set the URL of the game server.
*
* @param url The URL (include initial http://).
*/
@DesignerProperty(
editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING,
defaultValue = "http://appinvgameserver.appspot.com")
public void ServiceURL(String url){
if (url.endsWith("/")) {
this.serviceUrl = url.substring(0, url.length() - 1);
} else {
this.serviceUrl = url;
}
}
/**
* Returns the registered email address that is being used as the
* player id for this game client.
*/
@SimpleProperty(
description = "The email address that is being used as the " +
"player id for this game client. At present, users " +
"must set this manually in oder to join a game. But " +
"this property will change in the future so that is set " +
"automatically, and users will not be able to change it.",
category = PropertyCategory.BEHAVIOR)
public String UserEmailAddress() {
if (userEmailAddress.equals("")) {
Info("User email address is empty.");
}
return userEmailAddress;
}
/**
* Changes the player of this game by changing the email address
* used to communicate with the server.
*
* This should only be used during development. Games should not
* allow players to set their own email address.
*
* @param emailAddress The email address to set the current player
* id to.
*/
@SimpleProperty
public void UserEmailAddress(String emailAddress) {
userEmailAddress = emailAddress;
UserEmailAddressSet(emailAddress);
}
//----------------------------------------------------------------
// Event Handlers
/**
* Indicates that a server request from a function call has
* completed. This can be used to control a polling loop or
* otherwise respond to server request completions.
*
* @param functionName The name of the App Inventor function that
* finished.
*/
@SimpleEvent(description = "Indicates that a function call completed.")
public void FunctionCompleted(final String functionName) {
androidUIHandler.post(new Runnable() {
public void run() {
Log.d(LOG_TAG, "Request completed: " + functionName);
EventDispatcher.dispatchEvent(GameClient.this, "FunctionCompleted", functionName);
}});
}
/**
* Ensures that the GameId was set by the game creator.
*/
public void Initialize() {
Log.d(LOG_TAG, "Initialize");
if (gameId.equals("")) {
throw new YailRuntimeError("Game Id must not be empty.", "GameClient Configuration Error.");
}
}
/**
* Indicates that a GetMessages call received a message. This could
* be invoked multiple times for a single call to GetMessages.
*
* @param type The type of the message received.
* @param contents The message's contents. Consists of a list
* nested to arbitrary depth that includes string, boolean and
* number values.
*/
@SimpleEvent(description = "Indicates that a new message has " +
"been received.")
public void GotMessage(final String type, final String sender, final List<Object> contents) {
Log.d(LOG_TAG, "Got message of type " + type);
androidUIHandler.post(new Runnable() {
public void run() {
EventDispatcher.dispatchEvent(GameClient.this, "GotMessage", type, sender, contents);
}});
}
/**
* Indicates that InstanceId has changed due to the creation of a
* new instance or setting the InstanceId.
*
* @param instanceId The id of the instance the player is now in.
*/
@SimpleEvent(description = "Indicates that the InstanceId " +
"property has changed as a result of calling " +
"MakeNewInstance or SetInstance.")
public void InstanceIdChanged(final String instanceId) {
Log.d(LOG_TAG, "Instance id changed to " + instanceId);
androidUIHandler.post(new Runnable() {
public void run() {
EventDispatcher.dispatchEvent(GameClient.this, "InstanceIdChanged", instanceId);
}});
}
/**
* Indicates a user has been invited to this game instance by
* another player.
*
* @param instanceId The id of the new game instance.
*/
@SimpleEvent(
description = "Indicates that a user has been invited to " +
"this game instance.")
public void Invited(final String instanceId) {
Log.d(LOG_TAG, "Player invited to " + instanceId);
androidUIHandler.post(new Runnable() {
public void run() {
EventDispatcher.dispatchEvent(GameClient.this, "Invited", instanceId);
}});
}
/**
* Indicates this game instance has a new leader. This could happen
* in response to a call to SetLeader or by the side effects of a
* server command performed by any player in the game.
*
* Since the current leader is sent back with every server
* response, NewLeader can trigger after making any server call.
*
* @param playerId The email address of the new leader.
*/
@SimpleEvent(description = "Indicates that this game has a new " +
"leader as specified through SetLeader")
public void NewLeader(final String playerId) {
androidUIHandler.post(new Runnable() {
public void run() {
Log.d(LOG_TAG, "Leader change to " + playerId);
EventDispatcher.dispatchEvent(GameClient.this, "NewLeader", playerId);
}});
}
/**
* Indicates this game instance was created as specified via
* MakeNewInstance. The creating player is automatically the leader
* of the instance and the InstanceId property has already been set
* to this new instance.
*
* @param instanceId The id of the newly created game instance.
*/
@SimpleEvent(description = "Indicates that a new instance was " +
"successfully created after calling MakeNewInstance.")
public void NewInstanceMade(final String instanceId) {
androidUIHandler.post(new Runnable() {
public void run() {
Log.d(LOG_TAG, "New instance made: " + instanceId);
EventDispatcher.dispatchEvent(GameClient.this, "NewInstanceMade", instanceId);
}});
}
/**
* Indicates that a player has joined this game instance.
*
* @param playerId The email address of the new player.
*/
@SimpleEvent(description = "Indicates that a new player has " +
"joined this game instance.")
public void PlayerJoined(final String playerId) {
androidUIHandler.post(new Runnable() {
public void run() {
if (!playerId.equals(UserEmailAddress())) {
Log.d(LOG_TAG, "Player joined: " + playerId);
EventDispatcher.dispatchEvent(GameClient.this, "PlayerJoined", playerId);
}
}});
}
/**
* Indicates that a player has left this game instance.
*
* @param playerId The email address of the player that left.
*/
@SimpleEvent(description = "Indicates that a player has left " +
"this game instance.")
public void PlayerLeft(final String playerId) {
androidUIHandler.post(new Runnable() {
public void run() {
Log.d(LOG_TAG, "Player left: " + playerId);
EventDispatcher.dispatchEvent(GameClient.this, "PlayerLeft", playerId);
}});
}
/**
* Indicates that an attempt to complete a server command failed on
* the server.
* @param command The command requested.
* @param arguments The arguments sent to the command.
*/
@SimpleEvent(
description = "Indicates that a server command failed.")
public void ServerCommandFailure(final String command, final YailList arguments) {
androidUIHandler.post(new Runnable() {
public void run() {
Log.d(LOG_TAG, "Server command failed: " + command);
EventDispatcher.dispatchEvent(GameClient.this, "ServerCommandFailure", command, arguments);
}});
}
/**
* Indicates that a ServerCommand completed.
*
* @param command The key for the command that resulted in this
* response.
* @param response The server response. This consists of a list
* nested to arbitrary depth that includes string, boolean and
* number values.
*/
@SimpleEvent(description = "Indicates that a server command " +
"returned successfully.")
public void ServerCommandSuccess(final String command, final List<Object> response) {
Log.d(LOG_TAG, command + " server command returned.");
androidUIHandler.post(new Runnable() {
public void run() {
EventDispatcher.dispatchEvent(GameClient.this,
"ServerCommandSuccess", command, response);
}});
}
/**
* Indicates that the user email address property has been
* successfully set. This event should be used to initialize
* any web service functions.
*
* This separate event was required because the email address was
* unable to be first fetched from the the UI thread without
* causing programs to hang. GameClient will now start fetching
* the user email address in its constructor and trigger this event
* when it finishes.
*/
@SimpleEvent(description = "Indicates that the user email " +
"address has been set.")
public void UserEmailAddressSet(final String emailAddress) {
Log.d(LOG_TAG, "Email address set.");
androidUIHandler.post(new Runnable() {
public void run() {
EventDispatcher.dispatchEvent(GameClient.this, "UserEmailAddressSet", emailAddress);
}});
}
//----------------------------------------------------------------
// Message events
/**
* Indicates that something has occurred which the player should be
* somehow informed of.
*
* @param message the message.
*/
@SimpleEvent(description = "Indicates that something has " +
"occurred which the player should know about.")
public void Info(final String message) {
Log.d(LOG_TAG, "Info: " + message);
androidUIHandler.post(new Runnable() {
public void run() {
EventDispatcher.dispatchEvent(GameClient.this, "Info", message);
}});
}
/**
* Indicates that the attempt to communicate with the web service
* resulted in an error.
*
* @param functionName The name of the function call that caused this
* error.
* @param message the error message
*/
@SimpleEvent(description = "Indicates that an error occurred " +
"while communicating with the web server.")
public void WebServiceError(final String functionName, final String message) {
Log.e(LOG_TAG, "WebServiceError: " + message);
androidUIHandler.post(new Runnable() {
public void run() {
EventDispatcher.dispatchEvent(GameClient.this, "WebServiceError", functionName, message);
}});
}
//----------------------------------------------------------------
// Functions
/**
* Updates the current InstancesJoined and InstancesInvited lists.
*
* If the player has been invited to new instances an Invited
* event will be raised for each new instance.
*/
@SimpleFunction(description = "Updates the InstancesJoined and " +
"InstancesInvited lists. This procedure can be called " +
"before setting the InstanceId.")
public void GetInstanceLists() {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() { postGetInstanceLists(); }});
}
private void postGetInstanceLists() {
AsyncCallbackPair<JSONObject> readMessagesCallback = new AsyncCallbackPair<JSONObject>(){
public void onSuccess(final JSONObject response) {
processInstanceLists(response);
FunctionCompleted("GetInstanceLists");
}
public void onFailure(final String message) {
WebServiceError("GetInstanceLists", "Failed to get up to date instance lists.");
}
};
postCommandToGameServer(GET_INSTANCE_LISTS_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
readMessagesCallback);
}
private void processInstanceLists(JSONObject instanceLists){
try {
joinedInstances = JsonUtil.getStringListFromJsonArray(instanceLists.
getJSONArray(JOINED_LIST_KEY));
publicInstances = JsonUtil.getStringListFromJsonArray(instanceLists.
getJSONArray(PUBLIC_LIST_KEY));
List<String> receivedInstancesInvited = JsonUtil.getStringListFromJsonArray(instanceLists.
getJSONArray(INVITED_LIST_KEY));
if (!receivedInstancesInvited.equals(InvitedInstances())) {
List<String> oldList = invitedInstances;
invitedInstances = receivedInstancesInvited;
List<String> newInvites = new ArrayList<String>(receivedInstancesInvited);
newInvites.removeAll(oldList);
for (final String instanceInvited : newInvites) {
Invited(instanceInvited);
}
}
} catch (JSONException e) {
Log.w(LOG_TAG, e);
Info("Instance lists failed to parse.");
}
}
/**
* Retrieves messages of the specified type.
*
* Requests that only messages which have not been seen during
* the current session are returned. Messages will be processed
* in chronological order with the oldest first, however, only
* the count newest messages will be retrieved. This means that
* one could "miss out" on some messages if they request less than
* the number of messages created since the last request for
* that message type.
*
* Setting type to the empty string will fetch all message types.
* Even though those message types were not specifically requested,
* their most recent message time will be updated. This keeps
* players from receiving the same message again if they later
* request the specific message type.
*
* Note that the message receive times are not updated until after
* the messages are actually received. Thus, if multiple message
* requests are made before the previous ones return, they could
* send stale time values and thus receive the same messages more
* than once. To avoid this, application creators should wait for
* the get messages function to return before calling it again.
*
* @param type The type of message to retrieve. If the empty string
* is used as the message type then all message types will be
* requested.
* @param count The maximum number of messages to retrieve. This
* should be an integer from 1 to 1000.
*/
@SimpleFunction(
description = "Retrieves messages of the specified type.")
public void GetMessages(final String type, final int count) {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() { postGetMessages(type, count); }});
}
private void postGetMessages(final String requestedType, final int count) {
AsyncCallbackPair<JSONObject> myCallback = new AsyncCallbackPair<JSONObject>() {
public void onSuccess(final JSONObject result) {
try {
int count = result.getInt(COUNT_KEY);
JSONArray messages = result.getJSONArray(MESSAGES_LIST_KEY);
for (int i = 0; i < count; i++) {
JSONObject message = messages.getJSONObject(i);
String type = message.getString(TYPE_KEY);
String sender = message.getString(MESSAGE_SENDER_KEY);
String time = message.getString(MESSAGE_TIME_KEY);
List<Object> contents = JsonUtil.getListFromJsonArray(message.
getJSONArray(MESSAGE_CONTENT_KEY));
// Assumes that the server is going to return messages in
// chronological order.
if (requestedType.equals("")) {
instance.putMessageTime(requestedType, time);
}
instance.putMessageTime(type, time);
GotMessage(type, sender, contents);
}
} catch (JSONException e) {
Log.w(LOG_TAG, e);
Info("Failed to parse messages response.");
}
FunctionCompleted("GetMessages");
}
public void onFailure(String message) {
WebServiceError("GetMessages", message);
}
};
if (InstanceId().equals("")) {
Info("You must join an instance before attempting to fetch messages.");
return;
}
postCommandToGameServer(GET_MESSAGES_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
new BasicNameValuePair(COUNT_KEY, Integer.toString(count)),
new BasicNameValuePair(MESSAGE_TIME_KEY, instance.getMessageTime(requestedType)),
new BasicNameValuePair(TYPE_KEY, requestedType)),
myCallback);
}
/**
* Invites a player to this game instance.
*
* Players implicitly accept invitations when they join games by
* setting the instance id in their GameClient.
*
* Invitations remain active as long as the game instance exists.
*
* @param playerEmail a string containing the email address of the
* player to become leader. The email should be in one of the
* following formats:<br>"Name O. Person
* <name.o.person@gmail.com>"<br>"name.o.person@gmail.com".
*/
@SimpleFunction(
description = "Invites a player to this game instance.")
public void Invite(final String playerEmail) {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() { postInvite(playerEmail); }});
}
private void postInvite(final String inviteeEmail) {
AsyncCallbackPair<JSONObject> inviteCallback = new AsyncCallbackPair<JSONObject>(){
public void onSuccess(final JSONObject response) {
try {
String invitedPlayer = response.getString(INVITEE_KEY);
if (invitedPlayer.equals("")) {
Info(invitedPlayer + " was already invited.");
} else {
Info("Successfully invited " + invitedPlayer + ".");
}
} catch (JSONException e) {
Log.w(LOG_TAG, e);
Info("Failed to parse invite player response.");
}
FunctionCompleted("Invite");
}
public void onFailure(final String message) {
WebServiceError("Invite", message);
}
};
if (InstanceId().equals("")) {
Info("You must have joined an instance before you can invite new players.");
return;
}
postCommandToGameServer(INVITE_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
new BasicNameValuePair(INVITEE_KEY, inviteeEmail)),
inviteCallback);
}
/**
* Requests to leave the current instance. If the player is the
* current leader, the lead will be passed to another player.
*
* If there are no other players left in the instance after the
* current player leaves, the instance will become unjoinable.
*
* Upon successful completion of this command, the instance
* lists will be updated and InstanceId will be set back to the
* empty string.
*
* Note that while this call does clear the leader and player
* lists, no NewLeader or PlayerLeft events are raised.
*/
@SimpleFunction(description = "Leaves the current instance.")
public void LeaveInstance() {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
postLeaveInstance();
}
});
}
private void postLeaveInstance() {
AsyncCallbackPair<JSONObject> setInstanceCallback = new AsyncCallbackPair<JSONObject>(){
public void onSuccess(final JSONObject response) {
SetInstance("");
processInstanceLists(response);
FunctionCompleted("LeaveInstance");
}
public void onFailure(final String message) {
WebServiceError("LeaveInstance", message);
}
};
postCommandToGameServer(LEAVE_INSTANCE_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
setInstanceCallback);
}
/**
* Creates a new game instance. The instance has a unique
* instanceId, and the leader is the player who created it. The
* player that creates the game automatically joins it without
* being sent an invitation.
*
* The actual instance id could differ from the instanceId
* specified because the game server will enforce uniqueness. The
* actual instanceId will be provided to AppInventor when a
* NewInstanceMade event triggers upon successful completion of
* this server request.
*
* @param instanceId A string to use as for the instance
* id. If no other instance exists with this id, the new instance
* will have this id. However, since the id must be unique, if
* another instance exists with the same one, then a number
* will be appended to the end of this prefix.
* @param makePublic A boolean indicating whether or not the
* instance should be publicly viewable and able to be joined by
* anyone.
*/
@SimpleFunction(description = "Asks the server to create a new " +
"instance of this game.")
public void MakeNewInstance(final String instanceId, final boolean makePublic) {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() { postMakeNewInstance(instanceId, makePublic); }});
}
private void postMakeNewInstance(final String requestedInstanceId, final Boolean makePublic) {
AsyncCallbackPair<JSONObject> makeNewGameCallback = new AsyncCallbackPair<JSONObject>(){
public void onSuccess(final JSONObject response) {
processInstanceLists(response);
NewInstanceMade(InstanceId());
FunctionCompleted("MakeNewInstance");
}
public void onFailure(final String message) {
WebServiceError("MakeNewInstance", message);
}
};
postCommandToGameServer(NEW_INSTANCE_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, requestedInstanceId),
new BasicNameValuePair(INSTANCE_PUBLIC_KEY, makePublic.toString())),
makeNewGameCallback, true);
}
/**
* Creates a new message and sends it to the stated recipients.
*
* @param type A "key" for the message. This identifies the type of
* message so that when other players receive the message they know
* how to properly handle it.
* @param recipients If set to an empty list, the server will send
* this message with a blank set of recipients, meaning that all
* players in the instance are able to retrieve it. To limit the
* message receipt to a single person or a group of people,
* recipients should be a list of the email addresses of the people
* meant to receive the message. Each email should be in one of the
* following formats:<br>
* "Name O. Person <name.o.person@gmail.com>"<br>
* "name.o.person@gmail.com"
* @param contents the contents of the message. This can be any
* AppInventor data value.
*/
@SimpleFunction(description = "Sends a keyed message to all " +
"recipients in the recipients list. The message will " +
"consist of the contents list.")
public void SendMessage(final String type, final YailList recipients, final YailList contents) {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() { postNewMessage(type, recipients, contents); }});
}
private void postNewMessage(final String type, YailList recipients, YailList contents){
AsyncCallbackPair<JSONObject> myCallback = new AsyncCallbackPair<JSONObject>(){
public void onSuccess(final JSONObject response) {
FunctionCompleted("SendMessage");
}
public void onFailure(final String message) {
WebServiceError("SendMessage", message);
}
};
if (InstanceId().equals("")) {
Info("You must have joined an instance before you can send messages.");
return;
}
postCommandToGameServer(NEW_MESSAGE_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
new BasicNameValuePair(TYPE_KEY, type),
new BasicNameValuePair(MESSAGE_RECIPIENTS_KEY, recipients.toJSONString()),
new BasicNameValuePair(MESSAGE_CONTENT_KEY, contents.toJSONString()),
new BasicNameValuePair(MESSAGE_TIME_KEY, instance.getMessageTime(type))),
myCallback);
}
/**
* Submits a command to the game server. Server commands are
* custom actions that are performed on the server. The arguments
* required and return value of a server command depend on its
* implementation.
*
* For more information about server commands, consult the game
* server code at:
* http://code.google.com/p/app-inventor-for-android/
*
* @param command The name of the server command.
* @param arguments The arguments to pass to the server to specify
* how to execute the command.
*/
@SimpleFunction(description = "Sends the specified command to " +
"the game server.")
public void ServerCommand(final String command, final YailList arguments) {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() { postServerCommand(command, arguments); }});
}
private void postServerCommand(final String command, final YailList arguments){
AsyncCallbackPair<JSONObject> myCallback = new AsyncCallbackPair<JSONObject>() {
public void onSuccess(final JSONObject result) {
try {
ServerCommandSuccess(command, JsonUtil.getListFromJsonArray(result.
getJSONArray(MESSAGE_CONTENT_KEY)));
} catch (JSONException e) {
Log.w(LOG_TAG, e);
Info("Server command response failed to parse.");
}
FunctionCompleted("ServerCommand");
}
public void onFailure(String message) {
ServerCommandFailure(command, arguments);
WebServiceError("ServerCommand", message);
}
};
Log.d(LOG_TAG, "Going to post " + command + " with args " + arguments);
postCommandToGameServer(SERVER_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
new BasicNameValuePair(COMMAND_TYPE_KEY, command),
new BasicNameValuePair(COMMAND_ARGUMENTS_KEY, arguments.toJSONString())),
myCallback);
}
/**
* Specifies the game instance id. Taken together, the game ID and
* the instance ID uniquely identify the game.
*
* @param instanceId the name of the game instance to join.
*/
@SimpleFunction(description = "Sets InstanceId and joins the " +
"specified instance.")
public void SetInstance(final String instanceId) {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() {
if (instanceId.equals("")) {
Log.d(LOG_TAG, "Instance id set to empty string.");
if (!InstanceId().equals("")) {
instance = new GameInstance("");
InstanceIdChanged("");
FunctionCompleted("SetInstance");
}
} else {
postSetInstance(instanceId);
}
}
});
}
private void postSetInstance(String instanceId) {
AsyncCallbackPair<JSONObject> setInstanceCallback = new AsyncCallbackPair<JSONObject>(){
public void onSuccess(final JSONObject response) {
processInstanceLists(response);
FunctionCompleted("SetInstance");
}
public void onFailure(final String message) {
WebServiceError("SetInstance", message);
}
};
postCommandToGameServer(JOIN_INSTANCE_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, instanceId),
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
setInstanceCallback, true);
}
/**
* Specifies the game's leader. At any time, each game instance
* has only one leader, but the leader may change over time.
* Initially, the leader is the game instance creator. Application
* inventors determine special properties of the leader.
*
* The leader can only be set by the current leader of the game.
*
* @param playerEmail a string containing the email address of the
* player to become leader. The email should be in one of the
* following formats:
* <br>"Name O. Person <name.o.person@gmail.com>"
* <br>"name.o.person@gmail.com".
*/
@SimpleFunction(description = "Tells the server to set the " +
"leader to playerId. Only the current leader may " +
"successfully set a new leader.")
public void SetLeader(final String playerEmail) {
AsynchUtil.runAsynchronously(new Runnable() {
public void run() { postSetLeader(playerEmail); }});
}
private void postSetLeader(final String newLeader) {
AsyncCallbackPair<JSONObject> setLeaderCallback = new AsyncCallbackPair<JSONObject>(){
public void onSuccess(final JSONObject response) {
FunctionCompleted("SetLeader");
}
public void onFailure(final String message) {
WebServiceError("SetLeader", message);
}
};
if (InstanceId().equals("")) {
Info("You must join an instance before attempting to set a leader.");
return;
}
postCommandToGameServer(SET_LEADER_COMMAND,
Lists.<NameValuePair>newArrayList(
new BasicNameValuePair(GAME_ID_KEY, GameId()),
new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
new BasicNameValuePair(LEADER_KEY, newLeader)),
setLeaderCallback);
}
//----------------------------------------------------------------
// Activity Lifecycle Management
/**
* Called automatically by the operating system.
*
* Currently does nothing.
*/
public void onResume() {
Log.d(LOG_TAG, "Activity Resumed.");
}
/**
* Called automatically by the operating system.
*
* Currently does nothing.
*/
public void onStop() {
Log.d(LOG_TAG, "Activity Stopped.");
}
//----------------------------------------------------------------
// Utility Methods
private void postCommandToGameServer(final String commandName,
List<NameValuePair> params, final AsyncCallbackPair<JSONObject> callback) {
postCommandToGameServer(commandName, params, callback, false);
}
private void postCommandToGameServer(final String commandName,
final List<NameValuePair> params, final AsyncCallbackPair<JSONObject> callback,
final boolean allowInstanceIdChange) {
AsyncCallbackPair<JSONObject> thisCallback = new AsyncCallbackPair<JSONObject>() {
public void onSuccess(JSONObject responseObject) {
Log.d(LOG_TAG, "Received response for " + commandName + ": " + responseObject.toString());
try {
if (responseObject.getBoolean(ERROR_RESPONSE_KEY)) {
callback.onFailure(responseObject.getString(SERVER_RETURN_VALUE_KEY));
} else {
String responseGameId = responseObject.getString(GAME_ID_KEY);
if (!responseGameId.equals(GameId())) {
Info("Incorrect game id in response: + " + responseGameId + ".");
return;
}
String responseInstanceId = responseObject.getString(INSTANCE_ID_KEY);
if (responseInstanceId.equals("")) {
callback.onSuccess(responseObject.getJSONObject(SERVER_RETURN_VALUE_KEY));
return;
}
if (responseInstanceId.equals(InstanceId())) {
updateInstanceInfo(responseObject);
} else {
if (allowInstanceIdChange || InstanceId().equals("")) {
instance = new GameInstance(responseInstanceId);
updateInstanceInfo(responseObject);
InstanceIdChanged(responseInstanceId);
} else {
Info("Ignored server response to " + commandName + " for incorrect instance " +
responseInstanceId + ".");
return;
}
}
callback.onSuccess(responseObject.getJSONObject(SERVER_RETURN_VALUE_KEY));
}
} catch (JSONException e) {
Log.w(LOG_TAG, e);
callback.onFailure("Failed to parse JSON response to command " + commandName);
}
}
public void onFailure(String failureMessage) {
Log.d(LOG_TAG, "Posting to server failed for " + commandName + " with arguments " +
params + "\n Failure message: " + failureMessage);
callback.onFailure(failureMessage);
}
};
WebServiceUtil.getInstance().postCommandReturningObject(ServiceUrl(), commandName, params,
thisCallback);
}
private void updateInstanceInfo(JSONObject responseObject) throws JSONException {
boolean newLeader = false;
String leader = responseObject.getString(LEADER_KEY);
List<String> receivedPlayers = JsonUtil.getStringListFromJsonArray(responseObject.
getJSONArray(PLAYERS_LIST_KEY));
if (!Leader().equals(leader)) {
instance.setLeader(leader);
newLeader = true;
}
PlayerListDelta playersDelta = instance.setPlayers(receivedPlayers);
if (playersDelta != PlayerListDelta.NO_CHANGE) {
for (final String player : playersDelta.getPlayersRemoved()) {
PlayerLeft(player);
}
for (final String player : playersDelta.getPlayersAdded()) {
PlayerJoined(player);
}
}
if (newLeader) {
NewLeader(Leader());
}
}
}