7 package com.google.appinventor.components.runtime;
30 import android.app.Activity;
31 import android.os.Handler;
32 import android.util.Log;
34 import org.apache.http.NameValuePair;
35 import org.apache.http.message.BasicNameValuePair;
36 import org.json.JSONArray;
37 import org.json.JSONException;
38 import org.json.JSONObject;
40 import java.util.ArrayList;
41 import java.util.List;
78 @DesignerComponent(version = YaVersion.GAMECLIENT_COMPONENT_VERSION,
79 description =
"Provides a way for applications to communicate with online game servers",
80 category = ComponentCategory.INTERNAL,
82 iconName =
"images/gameClient.png")
85 permissionNames =
"android.permission.INTERNET, " +
86 "com.google.android.googleapps.permission.GOOGLE_AUTH")
90 private static final String LOG_TAG =
"GameClient";
93 private static final String GAME_ID_KEY =
"gid";
94 private static final String INSTANCE_ID_KEY =
"iid";
95 private static final String PLAYER_ID_KEY =
"pid";
96 private static final String INVITEE_KEY =
"inv";
97 private static final String LEADER_KEY =
"leader";
98 private static final String COUNT_KEY =
"count";
99 private static final String TYPE_KEY =
"type";
100 private static final String INSTANCE_PUBLIC_KEY =
"makepublic";
101 private static final String MESSAGE_RECIPIENTS_KEY =
"mrec";
102 private static final String MESSAGE_CONTENT_KEY =
"contents";
103 private static final String MESSAGE_TIME_KEY =
"mtime";
104 private static final String MESSAGE_SENDER_KEY =
"msender";
105 private static final String COMMAND_TYPE_KEY =
"command";
106 private static final String COMMAND_ARGUMENTS_KEY =
"args";
107 private static final String SERVER_RETURN_VALUE_KEY =
"response";
108 private static final String MESSAGES_LIST_KEY =
"messages";
109 private static final String ERROR_RESPONSE_KEY =
"e";
110 private static final String PUBLIC_LIST_KEY =
"public";
111 private static final String JOINED_LIST_KEY =
"joined";
112 private static final String INVITED_LIST_KEY =
"invited";
113 private static final String PLAYERS_LIST_KEY =
"players";
116 private static final String GET_INSTANCE_LISTS_COMMAND =
"getinstancelists";
117 private static final String GET_MESSAGES_COMMAND =
"messages";
118 private static final String INVITE_COMMAND =
"invite";
119 private static final String JOIN_INSTANCE_COMMAND =
"joininstance";
120 private static final String LEAVE_INSTANCE_COMMAND =
"leaveinstance";
121 private static final String NEW_INSTANCE_COMMAND =
"newinstance";
122 private static final String NEW_MESSAGE_COMMAND =
"newmessage";
123 private static final String SERVER_COMMAND =
"servercommand";
124 private static final String SET_LEADER_COMMAND =
"setleader";
127 private String serviceUrl;
128 private String gameId;
130 private Handler androidUIHandler;
131 private Activity activityContext;
133 private String userEmailAddress =
"";
136 private List<String> joinedInstances;
138 private List<String> invitedInstances;
140 private List<String> publicInstances;
148 super(container.
$form());
152 androidUIHandler =
new Handler();
153 activityContext = container.
$context();
154 form.registerForOnResume(
this);
155 form.registerForOnStop(
this);
161 serviceUrl =
"http://appinvgameserver.appspot.com";
190 description =
"The game name for this application. " +
191 "The same game ID can have one or more game instances.",
193 public String GameId() {
207 public
void GameId(String
id) {
216 description =
"The game instance id. Taken together," +
217 "the game ID and the instance ID uniquely identify the game.",
219 public String InstanceId() {
229 description =
"The set of game instances to which this player has been " +
230 "invited but has not yet joined. To ensure current values are " +
231 "returned, first invoke GetInstanceLists.",
233 public List<String> InvitedInstances() {
234 return invitedInstances;
243 description =
"The set of game instances in which this player is " +
244 "participating. To ensure current values are returned, first " +
245 "invoke GetInstanceLists.",
247 public List<String> JoinedInstances() {
248 return joinedInstances;
260 description =
"The game's leader. At any time, each game instance has " +
261 "only one leader, but the leader may change with time. " +
262 "Initially, the leader is the game instance creator. Application " +
263 "writers determine special properties of the leader. The leader " +
264 "value is updated each time a successful communication is made " +
267 public String Leader() {
278 description =
"The current set of players for this game instance. Each " +
279 "player is designated by an email address, which is a string. The " +
280 "list of players is updated each time a successful communication " +
281 "is made with the game server.",
283 public List<String> Players() {
293 description =
"The set of game instances that have been marked public. " +
294 "To ensure current values are returned, first " +
295 "invoke {@link #GetInstanceLists}. ",
297 public List<String> PublicInstances() {
298 return publicInstances;
305 description =
"The URL of the game server.",
307 public String ServiceUrl() {
318 defaultValue =
"http://appinvgameserver.appspot.com")
320 public
void ServiceURL(String url){
321 if (url.endsWith(
"/")) {
322 this.serviceUrl = url.substring(0, url.length() - 1);
324 this.serviceUrl = url;
333 description =
"The email address that is being used as the " +
334 "player id for this game client. At present, users " +
335 "must set this manually in oder to join a game. But " +
336 "this property will change in the future so that is set " +
337 "automatically, and users will not be able to change it.",
340 public String UserEmailAddress() {
341 if (userEmailAddress.equals(
"")) {
342 Info(
"User email address is empty.");
344 return userEmailAddress;
359 userEmailAddress = emailAddress;
360 UserEmailAddressSet(emailAddress);
374 @
SimpleEvent(description =
"Indicates that a function call completed.")
375 public
void FunctionCompleted(final String functionName) {
376 androidUIHandler.post(
new Runnable() {
378 Log.d(LOG_TAG,
"Request completed: " + functionName);
387 Log.d(LOG_TAG,
"Initialize");
388 if (gameId.equals(
"")) {
389 throw new YailRuntimeError(
"Game Id must not be empty.",
"GameClient Configuration Error.");
402 @
SimpleEvent(description =
"Indicates that a new message has " +
404 public
void GotMessage(final String type, final String sender, final List<Object> contents) {
405 Log.d(LOG_TAG,
"Got message of type " + type);
406 androidUIHandler.post(
new Runnable() {
418 @
SimpleEvent(description =
"Indicates that the InstanceId " +
419 "property has changed as a result of calling " +
420 "MakeNewInstance or SetInstance.")
421 public
void InstanceIdChanged(final String instanceId) {
422 Log.d(LOG_TAG,
"Instance id changed to " + instanceId);
423 androidUIHandler.post(
new Runnable() {
436 description =
"Indicates that a user has been invited to " +
437 "this game instance.")
438 public
void Invited(final String instanceId) {
439 Log.d(LOG_TAG,
"Player invited to " + instanceId);
440 androidUIHandler.post(
new Runnable() {
456 @
SimpleEvent(description =
"Indicates that this game has a new " +
457 "leader as specified through SetLeader")
458 public
void NewLeader(final String playerId) {
459 androidUIHandler.post(
new Runnable() {
461 Log.d(LOG_TAG,
"Leader change to " + playerId);
474 @
SimpleEvent(description =
"Indicates that a new instance was " +
475 "successfully created after calling MakeNewInstance.")
476 public
void NewInstanceMade(final String instanceId) {
477 androidUIHandler.post(
new Runnable() {
479 Log.d(LOG_TAG,
"New instance made: " + instanceId);
489 @
SimpleEvent(description =
"Indicates that a new player has " +
490 "joined this game instance.")
491 public
void PlayerJoined(final String playerId) {
492 androidUIHandler.post(
new Runnable() {
494 if (!playerId.equals(UserEmailAddress())) {
495 Log.d(LOG_TAG,
"Player joined: " + playerId);
506 @
SimpleEvent(description =
"Indicates that a player has left " +
507 "this game instance.")
508 public
void PlayerLeft(final String playerId) {
509 androidUIHandler.post(
new Runnable() {
511 Log.d(LOG_TAG,
"Player left: " + playerId);
523 description =
"Indicates that a server command failed.")
524 public
void ServerCommandFailure(final String command, final
YailList arguments) {
525 androidUIHandler.post(
new Runnable() {
527 Log.d(LOG_TAG,
"Server command failed: " + command);
541 @
SimpleEvent(description =
"Indicates that a server command " +
542 "returned successfully.")
543 public
void ServerCommandSuccess(final String command, final List<Object> response) {
544 Log.d(LOG_TAG, command +
" server command returned.");
545 androidUIHandler.post(
new Runnable() {
548 "ServerCommandSuccess", command, response);
563 @
SimpleEvent(description =
"Indicates that the user email " +
564 "address has been set.")
565 public
void UserEmailAddressSet(final String emailAddress) {
566 Log.d(LOG_TAG,
"Email address set.");
567 androidUIHandler.post(
new Runnable() {
582 @
SimpleEvent(description =
"Indicates that something has " +
583 "occurred which the player should know about.")
584 public
void Info(final String message) {
585 Log.d(LOG_TAG,
"Info: " + message);
586 androidUIHandler.post(
new Runnable() {
601 @
SimpleEvent(description =
"Indicates that an error occurred " +
602 "while communicating with the web server.")
603 public
void WebServiceError(final String functionName, final String message) {
604 Log.e(LOG_TAG,
"WebServiceError: " + message);
605 androidUIHandler.post(
new Runnable() {
620 @
SimpleFunction(description =
"Updates the InstancesJoined and " +
621 "InstancesInvited lists. This procedure can be called " +
622 "before setting the InstanceId.")
623 public
void GetInstanceLists() {
625 public void run() { postGetInstanceLists(); }});
628 private void postGetInstanceLists() {
630 public void onSuccess(
final JSONObject response) {
631 processInstanceLists(response);
632 FunctionCompleted(
"GetInstanceLists");
634 public void onFailure(
final String message) {
635 WebServiceError(
"GetInstanceLists",
"Failed to get up to date instance lists.");
639 postCommandToGameServer(GET_INSTANCE_LISTS_COMMAND,
640 Lists.<NameValuePair>newArrayList(
641 new BasicNameValuePair(GAME_ID_KEY, GameId()),
642 new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
643 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
644 readMessagesCallback);
647 private void processInstanceLists(JSONObject instanceLists){
649 joinedInstances = JsonUtil.getStringListFromJsonArray(instanceLists.
650 getJSONArray(JOINED_LIST_KEY));
652 publicInstances = JsonUtil.getStringListFromJsonArray(instanceLists.
653 getJSONArray(PUBLIC_LIST_KEY));
655 List<String> receivedInstancesInvited = JsonUtil.getStringListFromJsonArray(instanceLists.
656 getJSONArray(INVITED_LIST_KEY));
658 if (!receivedInstancesInvited.equals(InvitedInstances())) {
659 List<String> oldList = invitedInstances;
660 invitedInstances = receivedInstancesInvited;
661 List<String> newInvites =
new ArrayList<String>(receivedInstancesInvited);
662 newInvites.removeAll(oldList);
664 for (
final String instanceInvited : newInvites) {
665 Invited(instanceInvited);
669 }
catch (JSONException e) {
671 Info(
"Instance lists failed to parse.");
706 description =
"Retrieves messages of the specified type.")
707 public
void GetMessages(final String type, final
int count) {
709 public void run() { postGetMessages(type, count); }});
712 private void postGetMessages(
final String requestedType,
final int count) {
714 public void onSuccess(
final JSONObject result) {
716 int count = result.getInt(COUNT_KEY);
717 JSONArray messages = result.getJSONArray(MESSAGES_LIST_KEY);
718 for (
int i = 0; i < count; i++) {
719 JSONObject message = messages.getJSONObject(i);
720 String type = message.getString(TYPE_KEY);
721 String sender = message.getString(MESSAGE_SENDER_KEY);
722 String time = message.getString(MESSAGE_TIME_KEY);
724 getJSONArray(MESSAGE_CONTENT_KEY),
true);
727 if (requestedType.equals(
"")) {
731 GotMessage(type, sender, contents);
733 }
catch (JSONException e) {
735 Info(
"Failed to parse messages response.");
737 FunctionCompleted(
"GetMessages");
740 public void onFailure(String message) {
741 WebServiceError(
"GetMessages", message);
745 if (InstanceId().equals(
"")) {
746 Info(
"You must join an instance before attempting to fetch messages.");
750 postCommandToGameServer(GET_MESSAGES_COMMAND,
751 Lists.<NameValuePair>newArrayList(
752 new BasicNameValuePair(GAME_ID_KEY, GameId()),
753 new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
754 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
755 new BasicNameValuePair(COUNT_KEY, Integer.toString(count)),
756 new BasicNameValuePair(MESSAGE_TIME_KEY, instance.
getMessageTime(requestedType)),
757 new BasicNameValuePair(TYPE_KEY, requestedType)),
775 description =
"Invites a player to this game instance.")
776 public
void Invite(final String playerEmail) {
778 public void run() { postInvite(playerEmail); }});
781 private void postInvite(
final String inviteeEmail) {
783 public void onSuccess(
final JSONObject response) {
785 String invitedPlayer = response.getString(INVITEE_KEY);
787 if (invitedPlayer.equals(
"")) {
788 Info(invitedPlayer +
" was already invited.");
790 Info(
"Successfully invited " + invitedPlayer +
".");
792 }
catch (JSONException e) {
794 Info(
"Failed to parse invite player response.");
796 FunctionCompleted(
"Invite");
798 public void onFailure(
final String message) {
799 WebServiceError(
"Invite", message);
803 if (InstanceId().equals(
"")) {
804 Info(
"You must have joined an instance before you can invite new players.");
808 postCommandToGameServer(INVITE_COMMAND,
809 Lists.<NameValuePair>newArrayList(
810 new BasicNameValuePair(GAME_ID_KEY, GameId()),
811 new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
812 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
813 new BasicNameValuePair(INVITEE_KEY, inviteeEmail)),
831 @SimpleFunction(description =
"Leaves the current instance.")
832 public
void LeaveInstance() {
840 private void postLeaveInstance() {
842 public void onSuccess(
final JSONObject response) {
844 processInstanceLists(response);
845 FunctionCompleted(
"LeaveInstance");
847 public void onFailure(
final String message) {
848 WebServiceError(
"LeaveInstance", message);
852 postCommandToGameServer(LEAVE_INSTANCE_COMMAND,
853 Lists.<NameValuePair>newArrayList(
854 new BasicNameValuePair(GAME_ID_KEY, GameId()),
855 new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
856 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
857 setInstanceCallback);
881 @SimpleFunction(description =
"Asks the server to create a new " +
882 "instance of this game.")
883 public
void MakeNewInstance(final String instanceId, final
boolean makePublic) {
885 public void run() { postMakeNewInstance(instanceId, makePublic); }});
888 private void postMakeNewInstance(
final String requestedInstanceId,
final Boolean makePublic) {
890 public void onSuccess(
final JSONObject response) {
891 processInstanceLists(response);
892 NewInstanceMade(InstanceId());
893 FunctionCompleted(
"MakeNewInstance");
895 public void onFailure(
final String message) {
896 WebServiceError(
"MakeNewInstance", message);
900 postCommandToGameServer(NEW_INSTANCE_COMMAND,
901 Lists.<NameValuePair>newArrayList(
902 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
903 new BasicNameValuePair(GAME_ID_KEY, GameId()),
904 new BasicNameValuePair(INSTANCE_ID_KEY, requestedInstanceId),
905 new BasicNameValuePair(INSTANCE_PUBLIC_KEY, makePublic.toString())),
906 makeNewGameCallback,
true);
927 @SimpleFunction(description =
"Sends a keyed message to all " +
928 "recipients in the recipients list. The message will " +
929 "consist of the contents list.")
930 public
void SendMessage(final String type, final
YailList recipients, final
YailList contents) {
932 public void run() { postNewMessage(type, recipients, contents); }});
935 private void postNewMessage(
final String type,
YailList recipients,
YailList contents){
937 public void onSuccess(
final JSONObject response) {
938 FunctionCompleted(
"SendMessage");
940 public void onFailure(
final String message) {
941 WebServiceError(
"SendMessage", message);
945 if (InstanceId().equals(
"")) {
946 Info(
"You must have joined an instance before you can send messages.");
950 postCommandToGameServer(NEW_MESSAGE_COMMAND,
951 Lists.<NameValuePair>newArrayList(
952 new BasicNameValuePair(GAME_ID_KEY, GameId()),
953 new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
954 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
955 new BasicNameValuePair(TYPE_KEY, type),
956 new BasicNameValuePair(MESSAGE_RECIPIENTS_KEY, recipients.
toJSONString()),
957 new BasicNameValuePair(MESSAGE_CONTENT_KEY, contents.
toJSONString()),
958 new BasicNameValuePair(MESSAGE_TIME_KEY, instance.
getMessageTime(type))),
976 @SimpleFunction(description =
"Sends the specified command to " +
978 public
void ServerCommand(final String command, final
YailList arguments) {
980 public void run() { postServerCommand(command, arguments); }});
983 private void postServerCommand(
final String command,
final YailList arguments){
985 public void onSuccess(
final JSONObject result) {
988 getJSONArray(MESSAGE_CONTENT_KEY),
true));
989 }
catch (JSONException e) {
991 Info(
"Server command response failed to parse.");
993 FunctionCompleted(
"ServerCommand");
996 public void onFailure(String message) {
997 ServerCommandFailure(command, arguments);
998 WebServiceError(
"ServerCommand", message);
1002 Log.d(LOG_TAG,
"Going to post " + command +
" with args " + arguments);
1003 postCommandToGameServer(SERVER_COMMAND,
1004 Lists.<NameValuePair>newArrayList(
1005 new BasicNameValuePair(GAME_ID_KEY, GameId()),
1006 new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
1007 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
1008 new BasicNameValuePair(COMMAND_TYPE_KEY, command),
1009 new BasicNameValuePair(COMMAND_ARGUMENTS_KEY, arguments.
toJSONString())),
1019 @SimpleFunction(description =
"Sets InstanceId and joins the " +
1020 "specified instance.")
1021 public
void SetInstance(final String instanceId) {
1024 if (instanceId.equals(
"")) {
1025 Log.d(LOG_TAG,
"Instance id set to empty string.");
1026 if (!InstanceId().equals(
"")) {
1028 InstanceIdChanged(
"");
1029 FunctionCompleted(
"SetInstance");
1032 postSetInstance(instanceId);
1038 private void postSetInstance(String instanceId) {
1040 public void onSuccess(
final JSONObject response) {
1041 processInstanceLists(response);
1042 FunctionCompleted(
"SetInstance");
1044 public void onFailure(
final String message) {
1045 WebServiceError(
"SetInstance", message);
1049 postCommandToGameServer(JOIN_INSTANCE_COMMAND,
1050 Lists.<NameValuePair>newArrayList(
1051 new BasicNameValuePair(GAME_ID_KEY, GameId()),
1052 new BasicNameValuePair(INSTANCE_ID_KEY, instanceId),
1053 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress())),
1054 setInstanceCallback,
true);
1071 @SimpleFunction(description =
"Tells the server to set the " +
1072 "leader to playerId. Only the current leader may " +
1073 "successfully set a new leader.")
1074 public
void SetLeader(final String playerEmail) {
1076 public void run() { postSetLeader(playerEmail); }});
1079 private void postSetLeader(
final String newLeader) {
1081 public void onSuccess(
final JSONObject response) {
1082 FunctionCompleted(
"SetLeader");
1084 public void onFailure(
final String message) {
1085 WebServiceError(
"SetLeader", message);
1089 if (InstanceId().equals(
"")) {
1090 Info(
"You must join an instance before attempting to set a leader.");
1094 postCommandToGameServer(SET_LEADER_COMMAND,
1095 Lists.<NameValuePair>newArrayList(
1096 new BasicNameValuePair(GAME_ID_KEY, GameId()),
1097 new BasicNameValuePair(INSTANCE_ID_KEY, InstanceId()),
1098 new BasicNameValuePair(PLAYER_ID_KEY, UserEmailAddress()),
1099 new BasicNameValuePair(LEADER_KEY, newLeader)),
1112 Log.d(LOG_TAG,
"Activity Resumed.");
1121 Log.d(LOG_TAG,
"Activity Stopped.");
1127 private void postCommandToGameServer(
final String commandName,
1129 postCommandToGameServer(commandName, params, callback,
false);
1132 private void postCommandToGameServer(
final String commandName,
1133 final List<NameValuePair> params,
final AsyncCallbackPair<JSONObject> callback,
1134 final boolean allowInstanceIdChange) {
1135 AsyncCallbackPair<JSONObject> thisCallback =
new AsyncCallbackPair<JSONObject>() {
1136 public void onSuccess(JSONObject responseObject) {
1137 Log.d(LOG_TAG,
"Received response for " + commandName +
": " + responseObject.toString());
1140 if (responseObject.getBoolean(ERROR_RESPONSE_KEY)) {
1141 callback.onFailure(responseObject.getString(SERVER_RETURN_VALUE_KEY));
1143 String responseGameId = responseObject.getString(GAME_ID_KEY);
1144 if (!responseGameId.equals(GameId())) {
1145 Info(
"Incorrect game id in response: + " + responseGameId +
".");
1148 String responseInstanceId = responseObject.getString(INSTANCE_ID_KEY);
1149 if (responseInstanceId.equals(
"")) {
1150 callback.onSuccess(responseObject.getJSONObject(SERVER_RETURN_VALUE_KEY));
1154 if (responseInstanceId.equals(InstanceId())) {
1155 updateInstanceInfo(responseObject);
1157 if (allowInstanceIdChange || InstanceId().equals(
"")) {
1158 instance =
new GameInstance(responseInstanceId);
1159 updateInstanceInfo(responseObject);
1160 InstanceIdChanged(responseInstanceId);
1162 Info(
"Ignored server response to " + commandName +
" for incorrect instance " +
1163 responseInstanceId +
".");
1167 callback.onSuccess(responseObject.getJSONObject(SERVER_RETURN_VALUE_KEY));
1169 }
catch (JSONException e) {
1171 callback.onFailure(
"Failed to parse JSON response to command " + commandName);
1174 public void onFailure(String failureMessage) {
1175 Log.d(LOG_TAG,
"Posting to server failed for " + commandName +
" with arguments " +
1176 params +
"\n Failure message: " + failureMessage);
1177 callback.onFailure(failureMessage);
1181 WebServiceUtil.getInstance().postCommandReturningObject(ServiceUrl(), commandName, params,
1185 private void updateInstanceInfo(JSONObject responseObject)
throws JSONException {
1186 boolean newLeader =
false;
1187 String leader = responseObject.getString(LEADER_KEY);
1188 List<String> receivedPlayers = JsonUtil.getStringListFromJsonArray(responseObject.
1189 getJSONArray(PLAYERS_LIST_KEY));
1191 if (!Leader().equals(leader)) {
1196 PlayerListDelta playersDelta = instance.
setPlayers(receivedPlayers);
1197 if (playersDelta != PlayerListDelta.NO_CHANGE) {
1198 for (
final String player : playersDelta.getPlayersRemoved()) {
1201 for (
final String player : playersDelta.getPlayersAdded()) {
1202 PlayerJoined(player);
1207 NewLeader(Leader());