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;

}

}

}

}