SpawnPlayerActivity
package com.spawnlabs.endpoint.gamestick.player.ui;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import com.spawnlabs.endpoint.gamestick.player.GameStickApplication;
import com.spawnlabs.endpoint.gamestick.player.R;
import com.spawnlabs.endpoint.gamestick.player.event.GameEvent;
import com.spawnlabs.endpoint.gamestick.player.misc.HealthReportTimer;
import com.spawnlabs.endpoint.gamestick.player.model.Q3DemoConnectionProperties;
import com.spawnlabs.endpoint.gamestick.player.model.gameplayer.GameNode;
import com.spawnlabs.endpoint.gamestick.player.model.gameplayer.GamePlayerAlreadyPlayingException;
import com.spawnlabs.endpoint.gamestick.player.model.gameplayer.GamePlayerException;
import com.spawnlabs.endpoint.gamestick.player.model.gameplayer.GamePlayerWarningException;
import com.spawnlabs.endpoint.gamestick.player.model.play.Session;
import com.spawnlabs.endpoint.gamestick.player.model.scoring.ScoringResources;
import com.spawnlabs.endpoint.gamestick.player.service.play.PlayService;
import com.spawnlabs.endpoint.gamestick.player.service.play.PlaySessionLogService;
import com.spawnlabs.endpoint.gamestick.player.service.spawnrest.SpawnRestException;
import com.spawnlabs.endpoint.gamestick.player.service.spawnrest.SpawnRestNotLoggedInException;
import com.spawnlabs.endpoint.gamestick.player.service.spawnrest.SpawnRestValidationException;
import com.spawnlabs.endpoint.gamestick.player.ui.adroit.AdroitComponent;
import com.spawnlabs.endpoint.gamestick.player.ui.adroit.AdroitComponentParameters;
import com.spawnlabs.endpoint.gamestick.player.ui.adroit.AdroitPlayerException;
import com.spawnlabs.endpoint.gamestick.player.util.DialogUtil;
import com.spawnlabs.endpoint.gamestick.player.util.ScoringUtil;
import com.spawnlabs.endpoint.gamestick.player.util.StringUtil;
import com.spawnlabs.endpoint.scoring.ScoringClientException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SpawnPlayerActivity
extends MenuBaseActivity {
private static final String TAG = "GS-" + SpawnPlayerActivity.class.getSimpleName();
private static final String CONTROLTAG = TAG + "-CONTROL-";
public static final String PLAYED_GAMES = "played_games";
protected final Handler handler = new Handler();
protected GameNode gameNode;
protected AdroitComponent adroitComponent;
private FrameLayout fLayout;
private PlaySessionData playSessionData = new PlaySessionData();
public SpawnPlayerActivity() {
gameNode = new GameNode();
}
private static List<MenuButton> menuButtons = Arrays.asList(new MenuButton[]{
new MenuButton(R.id.navContinueButton, R.id.navContinueButtonShadow, new ContinueAction()),
new MenuButton(R.id.navSettingsButton, R.id.navSettingsButtonShadow, SettingsActivity.class),
new MenuButton(R.id.navControlsButton, R.id.navControlsButtonShadow, (MenuAction)null), // TODO: Make this do something
new MenuButton(R.id.navQuitButton, R.id.navQuitButtonShadow, new QuitAction()),
new MenuButton(R.id.navHelpButton, R.id.navHelpButtonShadow, HelpActivity.class)
});
@Override
protected List<MenuButton> getMenuButtons() {
return menuButtons;
}
// ========================================================================================================
// Lifecycle methods
// ========================================================================================================
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.v(TAG, "onCreate()");
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
LayoutInflater layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
fLayout = (FrameLayout) layoutInflater.inflate(R.layout.activity_spawn_player, null);
setContentView(fLayout);
adroitComponent = new AdroitComponent((RelativeLayout) fLayout.findViewById(R.id.surfaceRelativeLayout),
this,
new AdroitComponentCallback(),
createAdroitComponentParameters(true),
createAdroitComponentParameters(false));
super.onCreate(savedInstanceState);
}
@Override
protected void onStart() {
Log.v(TAG, "onStart()");
super.onStart();
beginPlaySession();
}
@Override
protected void onStop() {
Log.v(TAG, "onStop()");
super.onStop();
stopPlaying();
}
// ========================================================================================================
// Play session handling
// ========================================================================================================
protected void beginPlaySession() {
Log.v(TAG, "beginPlaySession()");
GameStickApplication.get().setPlaying(true);
playSessionData.clear();
playSessionData.gameId = getIntent().getStringExtra(NavMenuBaseActivity.GAME_ID_EXTRA);
playSessionData.userId = getUserId();
addGameToGamesPlayed();
new ScoringAsyncTask(playSessionData.gameId).execute();
}
/**
* Called from PlaySessionAsyncTask
*/
public void startPlaying(Session session) {
if (session == null || StringUtil.isEmpty(session.token)) {
Log.e(TAG, "startPlaying(" + (session == null ? "session=Null" : session.token) + ")");
showErrorDialogAndClose("Failed to acquire session id from game node: " + (session == null ? "[NULL session]" : session.hdPublicUri));
return;
}
Log.v(TAG, "startPlaying(" + session.token + "|" + session.hdPublicUri + ")");
getApp().getBus().fire(GameEvent.started());
// Preserve the sessionId
playSessionData.sessionId = session.token;
// The signals to know if we had a problem with either action below:
Exception exception = null;
try {
String[] split = session.hdPublicUri.split(":");// ie: tcp://10.0.1.152:20010
String host = split[1].substring(2);
String port = split[2];
String rtspUrl = startNodePlaySession(session.token, host, Integer.parseInt(port));
Log.i(TAG, "rtspUrl: " + rtspUrl);
adroitComponent.startAdroitPlayer(rtspUrl);
} catch (GamePlayerAlreadyPlayingException e) {
// Don't know why we would start play when it's already going. But for now, consider it a warning and do nothing.
Log.w(TAG, "Caught GamePlayerAlreadyPlayingException");
} catch (GamePlayerException e) {
exception = e; // Hit the signal
} catch (AdroitPlayerException e) {
exception = e; // Hit the signal
} catch (Exception e) {
exception = e; // Hit the signal
}
// Check the signal from above
if (exception != null) {
String msg = "Gameplay Initialization Error: ";
Log.e(TAG, msg, exception);
showErrorDialogAndClose(msg + exception.getMessage());
} else { // Everything went well
HealthReportTimer.start(gameNode, adroitComponent);
// wait a bit, then check if everything is okay before declaring victory.
handler.postDelayed(new CheckClientReceivingPacketsRunnable(session.hdPublicUri), 8000);
}
}
protected void stopPlaying() {
Log.v(TAG, "stopPlaying()");
HealthReportTimer.stop();
try {
adroitComponent.stopAdroitPlayer();
} catch (AdroitPlayerException e) {
// Just log it and move on, since the docs don't tell us what this means
Log.e(TAG, "adroitComponent.stopAdroitPlayer() failed");
}
try {
gameNode.terminatePlaySession();
} catch (GamePlayerWarningException e) {
// Just log it and move on
Log.e(TAG, "gameNode.terminatePlaySession() failed");
}
try {
adroitComponent.destroyPlayer1();
} catch (AdroitPlayerException e) {
Log.i(TAG, " destroyPlayer() failed");
}
// destroyPlayer2();
GameStickApplication.get().setPlaying(false);
PlaySessionLogService.uploadLogs(this, playSessionData.userId, playSessionData.sessionId, playSessionData.gameId, playSessionData.nodeId); // todo nodeId is still null...
getApp().getBus().fire(GameEvent.stopped());
}
// ========================================================================================================
// Control events (keys, mouse, joystick)
// ========================================================================================================
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Log.v(CONTROLTAG + ".onKey", "Down: " + keyCode);
// No matter what, START and DELETE bring up the pause dialog
if (keyCode == KeyEvent.KEYCODE_BUTTON_START || keyCode == 112/* delete */) {
return true;
}
// Reserve a key to stop playing
if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
finish();
return true;
}
if (isNavShowing() && keyCode == KeyEvent.KEYCODE_BUTTON_A) {
return true;
} else if (isNavShowing()) {
return super.onKeyUp(keyCode, event);
} else if (gameNode.isPlaying()) {
sendKeyInputToApu(keyCode, event, true);
}
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Log.v(CONTROLTAG + ".onKey", "Up: " + keyCode);
if (isNavShowing() && keyCode == KeyEvent.KEYCODE_BUTTON_A) {
return true;
} else if (isNavShowing() || keyCode == KeyEvent.KEYCODE_BUTTON_START ) {
return super.onKeyUp(keyCode, event);
} else if (gameNode.isPlaying()) {
sendKeyInputToApu(keyCode, event, false);
}
return true;
}
void sendKeyInputToApu(int keyCode, KeyEvent event, boolean keyDown) {
int keyPosition = keyDown ? 1 : 0;
boolean isNotMouse = (event.getDevice().getSources() & InputDevice.SOURCE_MOUSE) == 0;
if ((isNotMouse) && ((event.getSource() & InputDevice.SOURCE_KEYBOARD) != 0)) {
// Log.v(CONTROLTAG, "Calling sendKeyBoardInput() on node" + ": " + keyCode + " " + keyPosition);
try {
gameNode.sendKeyBoardInput(keyCode, keyPosition);
} catch (GamePlayerWarningException e) {
// Just log it and move on
Log.e(TAG, "gameNode.sendKeyBoardInput(keyCode, keyPosition) failed");
}
} else { // if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) != 0) {
// Log.v(CONTROLTAG, "Calling sendGamePadInput() on node" + ": " + keyCode + " " + keyPosition);
try {
gameNode.sendGamePadInput(keyCode, keyPosition);
} catch (GamePlayerWarningException e) {
// Just log it and move on
Log.e(TAG, "gameNode.sendGamePadInput(keyCode, keyPosition) failed");
}
}
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
// Log.v(CONTROLTAG, "onGenericMotionEvent() " + event.toString() + "Device id: " + event.getDeviceId() + "source: " + event.getSource());
if (isNavShowing()) {
return false;
}
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (gameNode.isPlaying()) {
JoystickEvent je = new JoystickEvent(event);
try {
gameNode.sendMotionInput(je.leftTrigger, je.rightTrigger, je.leftJoystickX, je.leftJoystickY, je.rightJoystickX, je.rightJoystickY);
} catch (GamePlayerWarningException e) {
// Just log it and move on
Log.e(TAG, "gameNode.sendMotionInput() failed");
}
}
}
if ((event.getSource() & InputDevice.SOURCE_MOUSE) != 0) {
if (gameNode.isPlaying()) {
MouseEvent me = new MouseEvent(event);
try {
gameNode.sendMouseInput(me.xAxis, me.yAxis, me.vScroll, me.buttons);
} catch (GamePlayerWarningException e) {
// Just log it and move on
Log.e(TAG, "gameNode.sendMouseInput() failed");
}
}
}
return true;
}
// ========================================================================================================
// UI methods
// ========================================================================================================
// ========================================================================================================
// GameNode control methods
// ========================================================================================================
String startNodePlaySession(String sessionId, String gameNodeIp, int gameNodePort) throws GamePlayerException, GamePlayerAlreadyPlayingException {
return gameNode.startPlaySession(gameNodeIp, String.valueOf(gameNodePort), sessionId);
}
// ========================================================================================================
// Minor methods
// ========================================================================================================
private void addGameToGamesPlayed() {
SharedPreferences sharedPreferences = getSharedPreferences(PLAYED_GAMES, Context.MODE_PRIVATE);
Set<String> playedGames = sharedPreferences.getStringSet(PLAYED_GAMES, new HashSet<String>());
playedGames.add(playSessionData.gameId);
sharedPreferences.edit().remove(PLAYED_GAMES).commit(); // Remove first, cuz there's a bug in putStringSet()
// that FAILS to commit to persistent storage!
sharedPreferences.edit().putStringSet(PLAYED_GAMES, playedGames).commit();
}
void showErrorDialogAndClose(String msg) {
if (!isFinishing()) {
alert(msg, new ErrorDialogCloseCallback());
}
}
/**
* Q3DEMO1 MPU:
* 66.195.107.45
* 10.0.3.110
* <p/>
* Q3DEMO2 MPU: 66.195.107.46 10.0.3.112
*/
String getGameNodeIp() {
return Q3DemoConnectionProperties.getGameNodeIp(playSessionData.gameId);
}
int getGameNodePort() {
return Q3DemoConnectionProperties.getGameNodePort();
}
int getGameNodeApiPort() {
return Q3DemoConnectionProperties.getGameNodeApiPort();
}
private AdroitComponentParameters createAdroitComponentParameters(boolean playerOne) {
if (playerOne) {
String inputPlyrString = getResources().getString(R.string.profile1Ip);
return new AdroitComponentParameters(inputPlyrString);
} else {
String inputPlyrString2 = getResources().getString(R.string.profile2Ip);
return new AdroitComponentParameters(inputPlyrString2);
}
}
private String getUserId() {
try {
return GameStickApplication.get().getUser().getUserId();
} catch (SpawnRestNotLoggedInException e) { // Shouldn't happen.
Log.e(TAG, "Somehow, the app allowed game streaming to start without the user being logged in. Program error.");
finish();// Really can't happen so don't bother calling showErrorDialogAndClose()
return null;
}
}
// ========================================================================================================
// Inner classes
// ========================================================================================================
class CheckClientReceivingPacketsRunnable
implements Runnable {
private final String TAG = CheckClientReceivingPacketsRunnable.class.getSimpleName();
private String hdPublicUri;
public CheckClientReceivingPacketsRunnable(String hdPublicUri) {
this.hdPublicUri = hdPublicUri;
}
@Override
public void run() {
try {
if (!HealthReportTimer.isClientReceivingPackets()) {
Log.e(TAG, "Client not receiving packets");
showErrorDialogAndClose("Session created successfully, but client is not receiving game stream packets from " + hdPublicUri);
}
} catch (HealthReportTimer.ClientNotStartedException e) {
Log.w(TAG, "HealthReportTimer queried about packets, but wasn't started yet.");
}
}
}
class JoystickEvent {
short leftTrigger;
short rightTrigger;
short leftJoystickX;
short leftJoystickY;
short rightJoystickX;
short rightJoystickY;
JoystickEvent(MotionEvent event) {
leftTrigger = (short) (event.getAxisValue(MotionEvent.AXIS_BRAKE) * 255);
rightTrigger = (short) (event.getAxisValue(MotionEvent.AXIS_GAS) * 255);
leftJoystickX = (short) (event.getAxisValue(MotionEvent.AXIS_X) * 32767);
leftJoystickY = (short) (event.getAxisValue(MotionEvent.AXIS_Y) * 32767);
rightJoystickX = (short) (event.getAxisValue(MotionEvent.AXIS_Z) * 32767);
rightJoystickY = (short) (event.getAxisValue(MotionEvent.AXIS_RZ) * 32767);
}
}
class MouseEvent {
int xAxis;
int yAxis;
short vScroll;
short buttons;
MouseEvent(MotionEvent event) {
xAxis = (int) (event.getAxisValue(MotionEvent.AXIS_X) * (65536 / 1280));
yAxis = (int) (event.getAxisValue(MotionEvent.AXIS_Y) * (65536 / 720));
vScroll = (short) (event.getAxisValue(MotionEvent.AXIS_VSCROLL) * (32767 / 720));
buttons = (short) event.getButtonState();
}
}
class ErrorDialogCloseCallback
implements Runnable {
@Override
public void run() {
finish();
}
}
class AdroitComponentCallback
implements AdroitComponent.AdroitComponentCallback {
@Override
public void surfaceDestroyed() {
SpawnPlayerActivity.this.finish();
}
@Override
public void surfaceRecreated() {
fLayout.bringToFront();
}
}
private class PlaySessionData {
private String userId;
private String sessionId;
private String gameId;
private String nodeId;
private void clear() {
userId = null;
sessionId = null;
gameId = null;
nodeId = null;
}
}
private static class QuitAction implements MenuAction {
@Override
public void performAction(MenuBaseActivity activity) {
activity.finish();
}
}
private static class ContinueAction implements MenuAction {
@Override
public void performAction(MenuBaseActivity activity) {
activity.deactivateNavMenu();
}
}
private class ScoringAsyncTask
extends AsyncTask<Void, Void, ScoringResources> {
private final String TAG = "GS-" + ScoringAsyncTask.class.getSimpleName();
private String errMsg;
private String gameId;
public ScoringAsyncTask(String gameId) {
this.gameId = gameId;
}
@Override
protected ScoringResources doInBackground(Void... params) {
Log.v(TAG, "doInBackground()");
try {
return ScoringUtil.doScoring(SpawnPlayerActivity.this);
} catch (SpawnRestException e) {
String msg = "Error retrieving scoring servers: ";
Log.e(TAG, msg, e);
errMsg = msg + e.getMessage();
return null;
} catch (ScoringClientException e) {
String msg = "Scoring failed: ";
Log.e(TAG, msg, e);
errMsg = msg + e.getMessage();
return null;
}
}
@Override
protected void onPostExecute(ScoringResources scoringResources) {
Log.v(TAG, "onPostExecute()");
if (scoringResources == null) {
Log.e(TAG, "Scoring failed");
if (errMsg != null) {
showErrorDialogAndClose(errMsg);
}
return;
}
Log.v(TAG, "completed scoringResources = " + scoringResources);
new PlaySessionAsyncTask(gameId).execute(scoringResources);
}
}
private class PlaySessionAsyncTask
extends AsyncTask<ScoringResources, Integer, PlaySessionAsyncTask.PlaySessionResult> {
private final String TAG = "GS-" + PlaySessionAsyncTask.class.getSimpleName();
private DialogUtil.DialogHandle dialogHandle;
private String gameId;
public PlaySessionAsyncTask(String gameId) {
this.gameId = gameId;
}
@Override
protected PlaySessionAsyncTask.PlaySessionResult doInBackground(ScoringResources... scoringResources) {
dialogHandle = DialogUtil.globalProgress(SpawnPlayerActivity.this, R.string.msg_acquiring_game_session, 0);
try {
// POST /plays
String playSessionId = PlayService.getPlaySessionId(SpawnPlayerActivity.this, scoringResources[0], gameId);
Log.v(TAG, "playSessionId = " + playSessionId);
// Poll GET /plays/:id
long start = System.currentTimeMillis();
Log.i(TAG, "Polling service for ready play session...");
while (!PlayService.isPlaySessionReady(SpawnPlayerActivity.this, playSessionId)) {
long now = System.currentTimeMillis();
if ((now - start) >= getResources().getInteger(R.integer.GET_SESSION_ID_TIMEOUT)) {
String timeoutMsg = getString(R.string.msg_timeout_waiting_for_playsession);
Log.e(TAG, timeoutMsg);
return new PlaySessionResult().fail(timeoutMsg);
}
waitAtick();
}
Log.i(TAG, "Play session " + playSessionId + " READY.");
// GET /plays/:id/session
Session playSession = PlayService.getPlaySession(SpawnPlayerActivity.this, playSessionId);
Log.v(TAG, "playSession = " + playSession);// todo -EJT cleanup
// ------------------------------------------------------------------------------------
try {
for (int i = 0; i < 7; i++) {// todo -EJT cleanup Temporary workaround for slow MPU
Thread.sleep(100);
dialogHandle.progress(i, 6);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// ------------------------------------------------------------------------------------
return new PlaySessionResult().win(playSession);
} catch (SpawnRestValidationException e) { // from getPlaySessionId()
String msg = getString(R.string.msg_inadequate_connectivity_for_playsession);
Log.e(TAG, msg, e);
return new PlaySessionResult().fail(msg + " " + e.getMessage());
} catch (SpawnRestException e) {
String msg = getString(R.string.msg_failed_to_get_sessionid_from_playservice);
Log.e(TAG, msg, e);
return new PlaySessionResult().fail(msg + " " + e.getMessage());
} finally {
dialogHandle.dismiss();
}
}
@Override
protected void onPostExecute(PlaySessionResult playSessionResult) {
if (!playSessionResult.success) {
showErrorDialogAndClose(playSessionResult.errMsg);
return;
}
startPlaying(playSessionResult.playSession);
}
private void waitAtick() {
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
}
class PlaySessionResult {
Session playSession;
boolean success;
private String errMsg;
PlaySessionResult win(Session playSession) {
this.playSession = playSession;
success = true;
return this;
}
PlaySessionResult fail(String errMsg) {
this.errMsg = errMsg;
success = false;
return this;
}
}
}
}