com.google.appinventor.components.runtime.GameClient.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.components.runtime.GameClient.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt

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
     * &ltname.o.person@gmail.com&gt"<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 &ltname.o.person@gmail.com&gt"<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 &ltname.o.person@gmail.com&gt"
     * <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());
        }
    }
}