diff --git a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/GmsTask.java b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/GmsTask.java new file mode 100644 index 000000000..caa68d628 --- /dev/null +++ b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/GmsTask.java @@ -0,0 +1,10 @@ +package au.com.codeka.warworlds.client.concurrency; + +/** Simple wrapper around GmsCore tasks API. */ +public class GmsTask extends Task { + public GmsTask(TaskRunner taskRunner, com.google.android.gms.tasks.Task gmsTask) { + super(taskRunner); + gmsTask.addOnFailureListener(this::onError); + gmsTask.addOnCompleteListener(task -> this.onComplete(task.getResult())); + } +} diff --git a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/RunnableQueue.java b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/RunnableQueue.java new file mode 100644 index 000000000..3433a0a44 --- /dev/null +++ b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/RunnableQueue.java @@ -0,0 +1,32 @@ +package au.com.codeka.warworlds.client.concurrency; + +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A queue of tasks that we can run at any time. Useful for posting runnables to threads. + */ +public class RunnableQueue { + private final Queue runnables; + + public RunnableQueue(int maxQueuedItems) { + runnables = new LinkedBlockingQueue<>(maxQueuedItems); + } + + public void post(Runnable runnable) { + synchronized (runnables) { + runnables.add(runnable); + } + } + + /** Runs all runnables on the queue. */ + public void runAllTasks() { + // TODO: should we pull these off into another list so the we can unblock the thread? + synchronized (runnables) { + while (!runnables.isEmpty()) { + Runnable runnable = runnables.remove(); + runnable.run(); + } + } + } +} diff --git a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/RunnableTask.java b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/RunnableTask.java new file mode 100644 index 000000000..697a69143 --- /dev/null +++ b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/RunnableTask.java @@ -0,0 +1,90 @@ +package au.com.codeka.warworlds.client.concurrency; + +/** + * A {@link Task} that encapsulates a {@link Runnable}, {@link RunnableP}, {@link RunnableR} or + * {@link RunnablePR} that you want to run on a particular thread. + * + * @param

The parameter type. Depending on the type of runnable you're using, this may or may + * not be ignored. + * @param The result type. Depending on the type of runnable you're using, this may or may + * not be ignored. + */ +public class RunnableTask extends Task { + /** A runnable that takes a parameter. */ + public interface RunnableP

{ + void run(P param); + } + + /** A runnable that returns a value. */ + public interface RunnableR { + R run(); + } + + /** A runnable that takes a parameter and returns a value. */ + public interface RunnablePR { + R run(P param); + } + + private final Runnable runnable; + private final RunnableP

runnableP; + private final RunnableR runnableR; + private final RunnablePR runnablePR; + private final Threads thread; + + public RunnableTask(TaskRunner taskRunner, Runnable runnable, Threads thread) { + super(taskRunner); + this.runnable = runnable; + this.runnableP = null; + this.runnableR = null; + this.runnablePR = null; + this.thread = thread; + } + + public RunnableTask(TaskRunner taskRunner, RunnableP

runnable, Threads thread) { + super(taskRunner); + this.runnable = null; + this.runnableP = runnable; + this.runnableR = null; + this.runnablePR = null; + this.thread = thread; + } + + public RunnableTask(TaskRunner taskRunner, RunnableR runnable, Threads thread) { + super(taskRunner); + this.runnable = null; + this.runnableP = null; + this.runnableR = runnable; + this.runnablePR = null; + this.thread = thread; + } + + public RunnableTask(TaskRunner taskRunner, RunnablePR runnable, Threads thread) { + super(taskRunner); + this.runnable = null; + this.runnableP = null; + this.runnableR = null; + this.runnablePR = runnable; + this.thread = thread; + } + + @Override + public void run(P param) { + thread.run(() -> { + try { + R result = null; + if (runnable != null) { + runnable.run(); + } else if (runnableP != null) { + runnableP.run(param); + } else if (runnableR != null) { + result = runnableR.run(); + } else if (runnablePR != null) { + result = runnablePR.run(param); + } + onComplete(result); + } catch (Exception e) { + onError(e); + } + }); + } +} diff --git a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/Task.java b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/Task.java new file mode 100644 index 000000000..ea8765c09 --- /dev/null +++ b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/Task.java @@ -0,0 +1,162 @@ +package au.com.codeka.warworlds.client.concurrency; + +import java.util.ArrayList; +import java.util.List; + +/** + * Wrapper for a "task", which allows chaining of following tasks (via {@link #then}) and handling + * of errors (via {@link #error}). + * + * @param

the type of the input parameter to this task. Can be Void if you want nothing. + * @param the type of the return value from the task. Can be Void if you want nothing. + */ +public class Task { + private final TaskRunner taskRunner; + private List> thenTasks; + private List> errorTasks; + private final Object lock = new Object(); + private boolean finished; + private R result; + private Exception error; + + Task(TaskRunner taskRunner) { + this.taskRunner = taskRunner; + } + + void run(P param) { + } + + protected void onComplete(R result) { + synchronized (lock) { + finished = true; + this.result = result; + if (thenTasks != null) { + for (Task task : thenTasks) { + taskRunner.runTask(task, result); + } + thenTasks = null; + } + } + } + + protected void onError(Exception error) { + synchronized (lock) { + finished = true; + this.error = error; + + if (errorTasks != null) { + for (Task task : errorTasks) { + taskRunner.runTask(task, null); + } + errorTasks = null; + } + } + } + + /** + * Queues up the given {@link Task} to run after this task. It will be handed this task's result + * as it's parameter. + * @param task The task to queue after this task completes. + * @return The new task (so you can chain .then().then().then() calls to get tasks to run one + * after the other. + */ + public Task then(Task task) { + synchronized (lock) { + if (finished) { + if (error == null) { + taskRunner.runTask(task, result); + } + return task; + } + if (this.thenTasks == null) { + this.thenTasks = new ArrayList<>(); + } + this.thenTasks.add(task); + } + + return task; + } + + /** + * Queues the given runnable to run after this task. If this task returns a result, obviously the + * runnable will not know what it was. + * + * @param runnable The runnable to run after this task completes. + * @param thread The {@link Threads} on which to run the runnable. + * @return The new task (so you can chain .then().then().then() calls to get tasks to run one + * after the other. + */ + public Task then(Runnable runnable, Threads thread) { + return then(new RunnableTask(taskRunner, runnable, thread)); + } + + /** + * Queues the given runnable to run after this task. If this task returns a result, obviously the + * runnable will not know what it was. + * + * @param runnable The runnable to run after this task completes. + * @param thread The {@link Threads} on which to run the runnable. + * @return The new task (so you can chain .then().then().then() calls to get tasks to run one + * after the other. + */ + public Task then(RunnableTask.RunnableP runnable, Threads thread) { + return then(new RunnableTask(taskRunner, runnable, thread)); + } + + /** + * Queues the given runnable to run after this task. If this task returns a result, obviously the + * runnable will not know what it was. + * + * @param runnable The runnable to run after this task completes. + * @param thread The {@link Threads} on which to run the runnable. + * @return The new task (so you can chain .then().then().then() calls to get tasks to run one + * after the other. + */ + public Task then(RunnableTask.RunnableR runnable, Threads thread) { + return then(new RunnableTask<>(taskRunner, runnable, thread)); + } + + /** + * Queues the given runnable to run after this task. If this task returns a result, obviously the + * runnable will not know what it was. + * + * @param runnable The runnable to run after this task completes. + * @param thread The {@link Threads} on which to run the runnable. + * @return The new task (so you can chain .then().then().then() calls to get tasks to run one + * after the other. + */ + public Task then(RunnableTask.RunnablePR runnable, Threads thread) { + return then(new RunnableTask<>(taskRunner, runnable, thread)); + } + + /** + * Queue a task to run in case there's an exception running the current task. + * + * @param task The task to run if there's an error. + * @return The current task, so you can queue up calls like task.error().then() to handle both + * the error case and the 'next' case. + */ + public Task error(Task task) { + synchronized (lock) { + if (finished) { + if (error != null) { + taskRunner.runTask(task, error); + } + return this; + } + if (this.errorTasks == null) { + this.errorTasks = new ArrayList<>(); + } + this.errorTasks.add(task); + } + return this; + } + + public Task error(Runnable runnable, Threads thread) { + return error(new RunnableTask<>(taskRunner, runnable, thread)); + } + + public Task error(RunnableTask.RunnableP runnable, Threads thread) { + return error(new RunnableTask<>(taskRunner, runnable, thread)); + } +} diff --git a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/TaskQueue.java b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/TaskQueue.java deleted file mode 100644 index 75b28cac1..000000000 --- a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/TaskQueue.java +++ /dev/null @@ -1,32 +0,0 @@ -package au.com.codeka.warworlds.client.concurrency; - -import java.util.Queue; -import java.util.concurrent.LinkedBlockingQueue; - -/** - * A queue of tasks that we can run at any time. Useful for posting runnables to threads. - */ -public class TaskQueue { - private final Queue tasks; - - public TaskQueue(int maxQueuedItems) { - tasks = new LinkedBlockingQueue<>(maxQueuedItems); - } - - public void postTask(Runnable runnable) { - synchronized (tasks) { - tasks.add(runnable); - } - } - - /** Runs all tasks on the queue. */ - public void runAllTasks() { - // TODO: should we pull these off into another list so the we can unblock the thread? - synchronized (tasks) { - while (!tasks.isEmpty()) { - Runnable runnable = tasks.remove(); - runnable.run(); - } - } - } -} diff --git a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/TaskRunner.java b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/TaskRunner.java index d6c15ee41..cc83b5e0c 100644 --- a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/TaskRunner.java +++ b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/TaskRunner.java @@ -1,5 +1,10 @@ package au.com.codeka.warworlds.client.concurrency; +import android.support.annotation.NonNull; + +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.OnFailureListener; + import java.util.Timer; import java.util.TimerTask; @@ -8,11 +13,11 @@ * in {@link Threads}. */ public class TaskRunner { - private ThreadPool backgroundThreadPool; + private Timer timer; public TaskRunner() { - backgroundThreadPool = new ThreadPool( + ThreadPool backgroundThreadPool = new ThreadPool( Threads.BACKGROUND, 750 /* maxQueuedItems */, 5 /* minThreads */, @@ -23,8 +28,57 @@ public TaskRunner() { timer = new Timer("Timer"); } - public void runTask(Runnable runnable, Threads thread) { - thread.runTask(runnable); + /** + * Run the given {@link Runnable} on the given {@link Threads}. + * + * @return A {@link Task} that you can use to chain further tasks after this one has finished. + */ + public Task runTask(Runnable runnable, Threads thread) { + return runTask(new RunnableTask(this, runnable, thread), null); + } + + /** + * Run the given {@link RunnableTask.RunnableP} on the given {@link Threads}. + * + * @return A {@link Task} that you can use to chain further tasks after this one has finished. + */ + public

Task runTask(RunnableTask.RunnableP

runnable, Threads thread) { + return runTask(new RunnableTask(this, runnable, thread), null); + } + + /** + * Run the given {@link RunnableTask.RunnableR} on the given {@link Threads}. + * + * @return A {@link Task} that you can use to chain further tasks after this one has finished. + */ + public Task runTask(RunnableTask.RunnableR runnable, Threads thread) { + return runTask(new RunnableTask(this, runnable, thread), null); + } + + /** + * Run the given {@link RunnableTask.RunnablePR} on the given {@link Threads}. + * + * @return A {@link Task} that you can use to chain further tasks after this one has finished. + */ + public Task runTask(RunnableTask.RunnablePR runnable, Threads thread) { + return runTask(new RunnableTask<>(this, runnable, thread), null); + } + + public

Task runTask(Task task, P param) { + task.run(param); + return task; + } + + /** + * Runs the given GmsCore {@link com.google.android.gms.tasks.Task}, and returns a {@link Task} + * that you can then use to chain other tasks, etc. + * + * @param gmsTask The GmsCore task to run. + * @param The type of result to expect from the GmsCore task. + * @return A {@link Task} that you can use to chain callbacks. + */ + public Task runTask(com.google.android.gms.tasks.Task gmsTask) { + return new GmsTask<>(this, gmsTask); } /** Run a task after the given delay. */ diff --git a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/ThreadPool.java b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/ThreadPool.java index 0f3fc97a5..6286f8d84 100644 --- a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/ThreadPool.java +++ b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/ThreadPool.java @@ -45,7 +45,7 @@ public Thread newThread(@NonNull Runnable r) { minThreads, maxThreads, keepAliveMs, TimeUnit.MILLISECONDS, workQueue, threadFactory); } - public void runTask(Runnable runnable) { + public void run(Runnable runnable) { executor.execute(runnable); } diff --git a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/Threads.java b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/Threads.java index c658c01e7..d15016666 100644 --- a/client/src/main/java/au/com/codeka/warworlds/client/concurrency/Threads.java +++ b/client/src/main/java/au/com/codeka/warworlds/client/concurrency/Threads.java @@ -42,14 +42,14 @@ public static void checkNotOnThread(Threads thread) { private boolean isInitialized; @Nullable private Handler handler; - @Nullable private TaskQueue taskQueue; + @Nullable private RunnableQueue runnableQueue; @Nullable private Thread thread; @Nullable private ThreadPool threadPool; - public void setThread(@NonNull Thread thread, @NonNull TaskQueue taskQueue) { - checkState(!isInitialized || this.taskQueue == taskQueue); + public void setThread(@NonNull Thread thread, @NonNull RunnableQueue runnableQueue) { + checkState(!isInitialized || this.runnableQueue == runnableQueue); this.thread = thread; - this.taskQueue = taskQueue; + this.runnableQueue = runnableQueue; this.isInitialized = true; } @@ -64,7 +64,7 @@ public void setThread(@NonNull Thread thread, @NonNull Handler handler) { public void resetThread() { thread = null; handler = null; - taskQueue = null; + runnableQueue = null; isInitialized = false; } @@ -86,13 +86,18 @@ public boolean isCurrentThread() { } } - public void runTask(Runnable runnable) { + /** + * Run the given {@link Runnable} on this thread. + * + * @param runnable The {@link Runnable} to run. + */ + public void run(Runnable runnable) { if (handler != null) { handler.post(runnable); } else if (threadPool != null) { - threadPool.runTask(runnable); - } else if (taskQueue != null) { - taskQueue.postTask(runnable); + threadPool.run(runnable); + } else if (runnableQueue != null) { + runnableQueue.post(runnable); } else { throw new IllegalStateException("Cannot run task, no handler, taskQueue or threadPool!"); } diff --git a/client/src/main/java/au/com/codeka/warworlds/client/net/Server.java b/client/src/main/java/au/com/codeka/warworlds/client/net/Server.java index e7864a1d7..8b52a697f 100644 --- a/client/src/main/java/au/com/codeka/warworlds/client/net/Server.java +++ b/client/src/main/java/au/com/codeka/warworlds/client/net/Server.java @@ -5,7 +5,7 @@ import android.os.Build; import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.iid.InstanceIdResult; import au.com.codeka.warworlds.client.App; import au.com.codeka.warworlds.client.concurrency.Threads; @@ -84,39 +84,40 @@ public void connect() { } private void login(@Nonnull String cookie) { - App.i.getTaskRunner().runTask(() -> { - log.info("Logging in: %s", ServerUrl.getUrl("/login")); - HttpRequest request = new HttpRequest.Builder() - .url(ServerUrl.getUrl("/login")) - .method(HttpRequest.Method.POST) - .body(new LoginRequest.Builder() - .cookie(cookie) - .device_info(populateDeviceInfo()) - .build().encode()) - .build(); - if (request.getResponseCode() != 200) { - if (request.getResponseCode() >= 401 && request.getResponseCode() < 500) { - // Our cookie must not be valid, we'll clear it before trying again. - GameSettings.i.edit() - .setString(GameSettings.Key.COOKIE, "") - .commit(); - } - log.error( - "Error logging in, will try again: %d", - request.getResponseCode(), - request.getException()); - disconnect(); - } else { - LoginResponse loginResponse = checkNotNull(request.getBody(LoginResponse.class)); - if (loginResponse.status != LoginResponse.LoginStatus.SUCCESS) { - updateState(ServerStateEvent.ConnectionState.ERROR, loginResponse.status); - log.error("Error logging in, got login status: %s", loginResponse.status); + App.i.getTaskRunner().runTask(FirebaseInstanceId.getInstance().getInstanceId()) + .then((InstanceIdResult instanceIdResult) -> { + log.info("Logging in: %s", ServerUrl.getUrl("/login")); + HttpRequest request = new HttpRequest.Builder() + .url(ServerUrl.getUrl("/login")) + .method(HttpRequest.Method.POST) + .body(new LoginRequest.Builder() + .cookie(cookie) + .device_info(populateDeviceInfo(instanceIdResult)) + .build().encode()) + .build(); + if (request.getResponseCode() != 200) { + if (request.getResponseCode() >= 401 && request.getResponseCode() < 500) { + // Our cookie must not be valid, we'll clear it before trying again. + GameSettings.i.edit() + .setString(GameSettings.Key.COOKIE, "") + .commit(); + } + log.error( + "Error logging in, will try again: %d", + request.getResponseCode(), + request.getException()); disconnect(); } else { - connectGameSocket(loginResponse); + LoginResponse loginResponse = checkNotNull(request.getBody(LoginResponse.class)); + if (loginResponse.status != LoginResponse.LoginStatus.SUCCESS) { + updateState(ServerStateEvent.ConnectionState.ERROR, loginResponse.status); + log.error("Error logging in, got login status: %s", loginResponse.status); + disconnect(); + } else { + connectGameSocket(loginResponse); + } } - } - }, Threads.BACKGROUND); + }, Threads.BACKGROUND); } private void connectGameSocket(LoginResponse loginResponse) { @@ -238,15 +239,7 @@ private void updateState( App.i.getEventBus().publish(currState); } - private static DeviceInfo populateDeviceInfo() { - String fcmToken = ""; - try { - fcmToken = FirebaseInstanceId.getInstance().getToken("wwmmo", "FCM"); - } catch (IOException e) { - log.error("Error getting FCM token.", e); - // We won't be able to message this device, I guess. - } -//FirebaseInstanceId.getInstance().getInstanceId() + private static DeviceInfo populateDeviceInfo(InstanceIdResult instanceIdResult) { return new DeviceInfo.Builder() .device_build(Build.ID) .device_id(GameSettings.i.getString(GameSettings.Key.INSTANCE_ID)) @@ -254,7 +247,8 @@ private static DeviceInfo populateDeviceInfo() { .device_model(Build.MODEL) .device_version(Build.VERSION.RELEASE) .fcm_device_info(new FcmDeviceInfo.Builder() - .token(fcmToken) + .token(instanceIdResult.getToken()) + .device_id(instanceIdResult.getId()) .build()) .build(); } diff --git a/client/src/main/java/au/com/codeka/warworlds/client/opengl/RenderSurfaceView.java b/client/src/main/java/au/com/codeka/warworlds/client/opengl/RenderSurfaceView.java index 99f9354d9..feb08bb7a 100644 --- a/client/src/main/java/au/com/codeka/warworlds/client/opengl/RenderSurfaceView.java +++ b/client/src/main/java/au/com/codeka/warworlds/client/opengl/RenderSurfaceView.java @@ -5,7 +5,7 @@ import android.opengl.GLSurfaceView; import android.support.annotation.Nullable; import android.util.AttributeSet; -import au.com.codeka.warworlds.client.concurrency.TaskQueue; +import au.com.codeka.warworlds.client.concurrency.RunnableQueue; import au.com.codeka.warworlds.client.concurrency.Threads; import au.com.codeka.warworlds.common.Log; import com.google.common.base.Preconditions; @@ -69,13 +69,13 @@ public static class Renderer implements GLSurfaceView.Renderer { private final TextureManager textureManager; private final Camera camera; @Nullable private Scene scene; - private TaskQueue taskQueue; + private RunnableQueue runnableQueue; private FrameCounter frameCounter; public Renderer(Context context) { this.multiSampling = true; this.textureManager = new TextureManager(context); - this.taskQueue = new TaskQueue(50 /* numQueuedItems */); + this.runnableQueue = new RunnableQueue(50 /* numQueuedItems */); this.dimensionResolver = new DimensionResolver(context); this.camera = new Camera(); this.frameCounter = new FrameCounter(); @@ -109,11 +109,11 @@ public void onSurfaceChanged(final GL10 ignored, final int width, final int heig @Override public void onDrawFrame(final GL10 ignored) { - Threads.GL.setThread(Thread.currentThread(), taskQueue); + Threads.GL.setThread(Thread.currentThread(), runnableQueue); frameCounter.onFrame(); // Empty the task queue - taskQueue.runAllTasks(); + runnableQueue.runAllTasks(); camera.onDraw(); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); diff --git a/client/src/main/java/au/com/codeka/warworlds/client/util/eventbus/EventHandlerInfo.java b/client/src/main/java/au/com/codeka/warworlds/client/util/eventbus/EventHandlerInfo.java index 7cd3b8a6d..4c4a5fd8d 100644 --- a/client/src/main/java/au/com/codeka/warworlds/client/util/eventbus/EventHandlerInfo.java +++ b/client/src/main/java/au/com/codeka/warworlds/client/util/eventbus/EventHandlerInfo.java @@ -69,7 +69,7 @@ public void call(final Object event) { if (callOnThread.isCurrentThread() && callOnThread != Threads.BACKGROUND) { runnable.run(); } else { - callOnThread.runTask(runnable); + callOnThread.run(runnable); } } } diff --git a/common/src/main/proto/au/com/codeka/warworlds/common/proto/account.proto b/common/src/main/proto/au/com/codeka/warworlds/common/proto/account.proto index 50e39bc3b..d57b59926 100644 --- a/common/src/main/proto/au/com/codeka/warworlds/common/proto/account.proto +++ b/common/src/main/proto/au/com/codeka/warworlds/common/proto/account.proto @@ -139,4 +139,7 @@ message DeviceInfo { message FcmDeviceInfo { // A token needed to message this device. optional string token = 1; + + // The device ID that firebase reports. + optional string device_id = 2; }