/* * Copyright 2014-2015 GameUp * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.gameup.android; import android.content.Context; import android.net.Uri; import android.support.annotation.Nullable; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; import com.squareup.okhttp.Request; import java.util.List; import io.gameup.android.entity.Achievement; import io.gameup.android.entity.Gamer; import io.gameup.android.entity.LeaderboardAndRank; import io.gameup.android.entity.Match; import io.gameup.android.entity.MatchList; import io.gameup.android.entity.Message; import io.gameup.android.entity.MessageList; import io.gameup.android.entity.Ping; import io.gameup.android.entity.PurchaseVerification; import io.gameup.android.entity.Rank; import io.gameup.android.entity.SharedStorage; import io.gameup.android.entity.TurnList; import io.gameup.android.http.RequestFactory; import io.gameup.android.entity.AchievementList; import io.gameup.android.http.RequestSender; import io.gameup.android.json.AchievementProgress; import io.gameup.android.json.GamerUpdate; import io.gameup.android.json.GsonFactory; import io.gameup.android.json.LeaderboardSubmission; import io.gameup.android.json.MatchAction; import io.gameup.android.json.MatchPlayers; import io.gameup.android.json.MatchTurn; import io.gameup.android.json.PurchaseVerify; import io.gameup.android.json.PushRegistration; import io.gameup.android.json.StorageGetWrapper; import io.gameup.android.push.Push; import lombok.Getter; import lombok.NonNull; /** * Represents a session for an authenticated user. * * All operations are thread-safe. * * Since this represents a particular user, the token is fixed - if it needs to * change then a new session should be created via the GameUp.login() method. * * Call the ping() method to check that the remote service is reachable, and * that the session is accepted with the given API key. */ @Getter public class GameUpSession { /** The API key this session is bound to. */ private final String apiKey; /** The user identification token for this session. */ private final String token; /** Rough local device timestamp when this session was created. */ private final long createdAt; /** * Initialise with the given token. * * @param apiKey The API key to use, must not be null. * @param token The token key to use, must not be null. */ GameUpSession(final @NonNull String apiKey, final @NonNull String token) { this.apiKey = apiKey; this.token = token; this.createdAt = System.currentTimeMillis(); } /** * Serialise this instance to a String, safe for storage or transmission. * The resulting String is ~230 bytes long. * * @return A String representation of this instance. */ @Override public String toString() { return GsonFactory.get().toJson(this); } /** * Load a session from the given String. Will fail hard if the input does * not represent a GameUpSession instance. * * @param gameUpSession The String to attempt to load from. * @return The retrieved GameUpSession instance. */ public static GameUpSession fromString(final @NonNull String gameUpSession) { try { return GsonFactory.get().fromJson( gameUpSession, GameUpSession.class); } catch (final JsonParseException e) { throw new RuntimeException("Input is not a valid GameUpSession", e); } } /** * Ping the GameUp service to check it is reachable and ready to handle * requests. * * Completes successfully if the service is reachable, responds correctly, * and accepts the API key and token combination. * * Throws a GameUpException otherwise. */ public void ping() { final Request request = RequestFactory.head( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .build(), apiKey, token); RequestSender.send(request); } /** * Ping the GameUp service to check it is reachable and ready to handle * requests. * * Completes successfully if the service is reachable, responds correctly, * and accepts the API key and token combination. * * Throws a GameUpException otherwise. * * @return A Ping object containing the server time. */ public Ping pingGet() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .build(), apiKey, token); return RequestSender.send(request, Ping.class); } /** * Get information about the gamer who owns this session. * * @return An entity containing gamer information. */ public Gamer gamer() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .build(), apiKey, token); return RequestSender.send(request, Gamer.class); } /** * Update a gamer's account or profile information. Currently only nickname * changes are exposed. */ public void gamer(final @NonNull String nickname) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.ACCOUNTS_SERVER) .appendPath("v0") .appendPath("gamer") .build(), apiKey, token, GsonFactory.get().toJson(new GamerUpdate(nickname))); RequestSender.send(request); } /** * Perform a key-value storage write operation, storing data as JSON. Data * is private per-user and per-game. * * NOTE: This is not designed to store confidential data, such as payment * information etc. * * @param key The key to store the given data under. * @param value The object to serialise and store. */ public void storagePut(final @NonNull String key, final @NonNull Object value) { final Request request = RequestFactory.put( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("storage") .appendPath(key) .build(), apiKey, token, GsonFactory.get().toJson(value)); RequestSender.send(request); } /** * Perform a key-value storage read operation. * * @param key The key to attempt to read data from. * @param type The class literal to attempt to deserialise as. * @return The entity requested, or null if there was no data. */ public <T> T storageGet(final @NonNull String key, final @NonNull Class<T> type) { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("storage") .appendPath(key) .build(), apiKey, token); final StorageGetWrapper wrapper; try { wrapper = RequestSender.send(request, StorageGetWrapper.class); } catch (final GameUpException e) { if (e.getStatus() == 404) { return null; } else { throw e; } } try { return GsonFactory.get().fromJson(wrapper.getValue(), type); } catch (final JsonParseException e) { throw new GameUpException(400, "Response data does not match expected entity"); } } /** * Perform a key-value storage delete operation. Will silently ignore absent * data. * * @param key The key to delete data from. */ public void storageDelete(final @NonNull String key) { final Request request = RequestFactory.delete( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("storage") .appendPath(key) .build(), apiKey, token); RequestSender.send(request); } /** * Directly get a shared storage key. * * @param key The key to look up and retrieve. * @return A SharedStorage instance containing the requested data. */ public SharedStorage sharedStorageGet(final @NonNull String key) { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("storage") .appendPath(key) .build(), apiKey, token); return RequestSender.send(request, SharedStorage.class); } /** * Completely replace the public portion of a shared storage key with the * given value. * * @param key The key to replace the public portion for. * @param value The value to replace it with. */ public void sharedStoragePut(final @NonNull String key, final @NonNull Object value) { final Request request = RequestFactory.put( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("shared") .appendPath(key) .appendPath("public") .build(), apiKey, token, GsonFactory.get().toJson(value)); RequestSender.send(request); } /** * Perform a JSON merge on the public portion of a shared storage key. * * @param key The key to update the public portion for. * @param value The value to use in the merge operation. */ public void sharedStorageUpdate(final @NonNull String key, final @NonNull Object value) { final Request request = RequestFactory.patch( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("shared") .appendPath(key) .appendPath("public") .build(), apiKey, token, GsonFactory.get().toJson(value)); RequestSender.send(request); } /** * Delete the public portion of a shared storage key. * * @param key The key to delete the public portion for. */ public void sharedStorageDelete(final @NonNull String key) { final Request request = RequestFactory.delete( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("shared") .appendPath(key) .appendPath("public") .build(), apiKey, token); RequestSender.send(request); } /** * Report progress towards a given achievement. Equivalent to calling * achievement(apiKey, achievementId, 1) below. * * Progress will be "1". This method is intended for convenience when * triggering "normal"-type achievements, but will still add 1 to an * "incremental"-type achievement if needed. * * @param achievementId The internal Achievement ID to interact with. * @return An Achievement instance if this call results in an achievement * being completed or progress is reported, null otherwise. */ public Achievement achievement(final @NonNull String achievementId) { return achievement(achievementId, 1); } /** * Report progress towards a given achievement. * * @param achievementId The internal Achievement ID to interact with. * @param count The progress amount to report. * @return An Achievement instance if this call results in an achievement * being completed or progress is reported, null otherwise. */ public Achievement achievement(final @NonNull String achievementId, final int count) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("achievement") .appendPath(achievementId) .build(), apiKey, token, GsonFactory.get().toJson(new AchievementProgress(count))); return RequestSender.send(request, Achievement.class); } /** * Get a list of achievements available for the game, including any gamer * data such as progress or completed timestamps. * * @return A List containing Achievement instances, may be empty if none are * returned for the current game. */ public AchievementList achievement() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("achievement") .build(), apiKey, token); return RequestSender.send(request, AchievementList.class); } /** * Submit a new score to the specified leaderboard. The new score will only * overwrite any previously submitted value if it's "better" according to * the sorting rules of the leaderboard, but updated ranking details are * returned in all cases. * * @param leaderboardId The private ID of the leaderboard to submit to. * @param score The score to submit. * @return A Rank instance containing updated detailed rank data for the * current gamer. */ public Rank leaderboard(final @NonNull String leaderboardId, final long score) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("leaderboard") .appendPath(leaderboardId) .build(), apiKey, token, GsonFactory.get().toJson(new LeaderboardSubmission(score))); return RequestSender.send(request, Rank.class); } /** * Request leaderboard metadata, the current top ranked gamers, and the * current gamer's detailed ranking on a specified leaderboard. * * @param leaderboardId The private ID of the leaderboard to request. * @return A corresponding LeaderboardAndRank instance. */ public LeaderboardAndRank leaderboard(final @NonNull String leaderboardId) { return leaderboard(leaderboardId, 50, 0, false); } /** * Request leaderboard metadata, and a page of entries on the leaderboard, * containing up to 'limit' number of entries and paginated to the page that * contains the current gamer's score on this leaderboard, if possible. * * Includes the current gamer's detailed ranking on this leaderboard. * * @param leaderboardId The private ID of the leaderboard to request. * @param limit The maximum number of leaderboard entries to return. * @param withScoretags true if each entry's scoretags should be included in * the response, false otherwise. * @return A corresponding LeaderboardAndRank instance. */ public LeaderboardAndRank leaderboard(final @NonNull String leaderboardId, final int limit, final boolean withScoretags) { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("leaderboard") .appendPath(leaderboardId) .appendQueryParameter("limit", Integer.toString(limit)) .appendQueryParameter("auto_offset", "true") .appendQueryParameter("with_scoretags", Boolean.toString(withScoretags)) .build(), apiKey, token); return RequestSender.send(request, LeaderboardAndRank.class); } /** * Request leaderboard metadata, and a page of entries on the leaderboard, * identified by the limit and offset parameters provided. * * Includes the current gamer's detailed ranking on this leaderboard. * * @param leaderboardId The private ID of the leaderboard to request. * @param limit The maximum number of leaderboard entries to return. * @param offset The offset to start entries from, used for pagination. * @param withScoretags true if each entry's scoretags should be included in * the response, false otherwise. * @return A corresponding LeaderboardAndRank instance. */ public LeaderboardAndRank leaderboard(final @NonNull String leaderboardId, final int limit, final int offset, final boolean withScoretags) { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("leaderboard") .appendPath(leaderboardId) .appendQueryParameter("limit", Integer.toString(limit)) .appendQueryParameter("offset", Integer.toString(offset)) .appendQueryParameter("with_scoretags", Boolean.toString(withScoretags)) .build(), apiKey, token); return RequestSender.send(request, LeaderboardAndRank.class); } /** * Get a list of all matches the gamer is part of. * * @return A MatchList entity containing all matches relevant to the gamer. */ public MatchList matchList() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("match") .build(), apiKey, token); return RequestSender.send(request, MatchList.class); } /** * Get status and metadata for a single match identified by its string ID. * * @param matchId The String match identifier to retrieve data for. * @return The corresponding Match entity. */ public Match matchGet(final @NonNull String matchId) { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("match") .appendPath(matchId) .build(), apiKey, token); return RequestSender.send(request, Match.class); } /** * Request a new match to be created by the GameUp service, with the total * number of gamers indicated; this player count includes the gamer making * the request. * * If there aren't enough waiting gamers to populate the match with, the * gamer will be added to the match queue instead, and the method will * return null. * * @param players The total number of gamers to populate the new match with. * @return The newly created Match entity, or null if the gamer was queued. */ @Nullable public Match matchCreate(final int players) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("match") .build(), apiKey, token, GsonFactory.get().toJson(new MatchPlayers(players))); return RequestSender.send(request, Match.class); } /** * Leave an existing match. Leaving is allowed only when it is NOT the * gamer's turn. * * @param matchId The match ID to leave. */ public void matchLeave(final @NonNull String matchId) { matchAction(matchId, MatchAction.Type.LEAVE); } /** * End an existing match. Ending is allowed only when it is the gamer's * turn. Ending closes the match, no more data submissions will be allowed. * * @param matchId The match ID to end. */ public void matchEnd(final @NonNull String matchId) { matchAction(matchId, MatchAction.Type.END); } private void matchAction(final @NonNull String matchId, final @NonNull MatchAction.Type action) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("match") .appendPath(matchId) .build(), apiKey, token, GsonFactory.get().toJson(new MatchAction(action))); RequestSender.send(request); } /** * Get all turns for the specified match. * * @param matchId The match ID to retrieve turns for. * @return A TurnList containing all existing turns for the specified match. */ public TurnList matchGetTurns(final @NonNull String matchId) { return matchGetTurns(matchId, 0); } /** * For the specified match, get turns newer than the turn number specified. * * For example if lastTurn is 4, the response will contain turns 5+. * * @param matchId The match ID to retrieve turns for. * @param lastTurn The lower threshold for turn numbers. * @return A TurnList containing only the specified turns for the selected * match. */ public TurnList matchGetTurns(final @NonNull String matchId, final int lastTurn) { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("match") .appendPath(matchId) .appendPath("turn") .appendPath(Integer.toString(lastTurn)) .build(), apiKey, token); return RequestSender.send(request, TurnList.class); } /** * Submit a new turn to the specified match. * * @param matchId The match ID to submit the turn to. * @param lastTurn The number of the last turn seen by this client. * @param nextGamer The nickname of the gamer to select as the next * turn-taker. * @param data Arbitrary turn data. Can be a string or serialised object. */ public void matchSubmitTurn(final @NonNull String matchId, final int lastTurn, final @NonNull String nextGamer, final @NonNull String data) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("match") .appendPath(matchId) .appendPath("turn") .build(), apiKey, token, GsonFactory.get().toJson( new MatchTurn(lastTurn, nextGamer, data))); RequestSender.send(request); } /** * Maintain the gamer's subscription to push messages. Must be called after * login, and if possible each time the application resumes from sleep or is * restarted. * * Includes a handler to be called each time a notification is opened. * * @param context The parent context calling this operation. * @param multiplayer Whether the gamer wishes to be subscribed to * background push notifications about multiplayer data. * @param segments Custom segments the gamer will receive updates about. */ public void push(final @NonNull Context context, final boolean multiplayer, final @NonNull List<String> segments) { final String id = Push.registerForPush(context); final Request request = RequestFactory.put( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("push") .build(), apiKey, token, GsonFactory.get().toJson(new PushRegistration( id, multiplayer, segments))); RequestSender.send(request); } /** * Verify an individual, one-off product purchase. * * @param purchase_token The token returned in the original product purchase * response from Google. * @param productId The ID of the product that was purchased. * @return A PurchaseVerification entity containing details about the * purchase status and recommended actions. */ public PurchaseVerification purchaseVerifyProduct( final @NonNull String purchase_token, final @NonNull String productId) { return purchaseVerify(purchase_token, productId, PurchaseVerify.Type.PRODUCT); } /** * Verify a subscription purchase, may or may not be auto-renewable. * * @param purchase_token The token returned in the original subscription * purchase response from Google. * @param subscriptionId The ID of the subscription that was purchased. * @return A PurchaseVerification entity containing details about the * purchase status and recommended actions. */ public PurchaseVerification purchaseVerifySubscription( final @NonNull String purchase_token, final @NonNull String subscriptionId) { return purchaseVerify(purchase_token, subscriptionId, PurchaseVerify.Type.SUBSCRIPTION); } private PurchaseVerification purchaseVerify( final @NonNull String purchase_token, final @NonNull String productId, final @NonNull PurchaseVerify.Type type) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("purchase") .appendPath("verify") .appendPath("google") .build(), apiKey, token, GsonFactory.get().toJson(new PurchaseVerify( purchase_token, productId, type))); return RequestSender.send(request, PurchaseVerification.class); } /** * Execute the given script, within the scope of the current gamer session. * * Ignore the result, useful when expected return is HTTP 204 No Body. * Send no input to the script. * * @param scriptId The identifier of the script to run. */ public void script(final @NonNull String scriptId) { GameUp.scriptNoReturn(scriptId, null, token); } /** * Execute the given script, within the scope of the current gamer session. * * Ignore the result, useful when expected return is HTTP 204 No Body. * Send `data` as input to the script. * * @param scriptId The identifier of the script to run. * @param data The data to send to the script as input. */ public void script(final @NonNull String scriptId, final @NonNull Object data) { GameUp.scriptNoReturn(scriptId, data, token); } /** * Execute the given script, within the scope of the current gamer session. * * Expect a result of the given `type`. * Send no input to the script. * * @param scriptId The identifier of the script to run. * @param type The class type to deserialize the response to. * @return An instance of the expected type. */ public <T> T script(final @NonNull String scriptId, final @NonNull Class<T> type) { return GameUp.scriptWithReturn(scriptId, type, null, token); } /** * Execute the given script, within the scope of the current gamer session. * * Expect a result of the given `type`. * Send `data` as input to the script. * * @param scriptId The identifier of the script to run. * @param type The class type to deserialize the response to. * @param data The data to send to the script as input. * @return An instance of the expected type. */ public <T> T script(final @NonNull String scriptId, final @NonNull Class<T> type, final @NonNull Object data) { return GameUp.scriptWithReturn(scriptId, type, data, token); } /** * Get all available messages, without their bodies. * * @return A corresponding MessageList instance. */ public MessageList messageList() { return messageList(0, false); } /** * Get all messages created after the UTC timestamp specified, without their * message bodies. * * @param since A UTC timestamp to use as a message filter, only messages * with a `createdAt` value greater than this will be returned. * @return A corresponding MessageList instance. */ public MessageList messageList(final long since) { return messageList(since, false); } /** * Get all messages created after the UTC timestamp specified, with or * without their message bodies based on the `withBody` flag. * * @param since A UTC timestamp to use as a message filter, only messages * with a `createdAt` value greater than this will be returned. * @param withBody true if message bodies should be returned, * false otherwise. * @return A corresponding MessageList instance. */ public MessageList messageList(final long since, final boolean withBody) { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("message") .appendQueryParameter("since", Long.toString(since)) .appendQueryParameter("with_body", Boolean.toString(withBody)) .build(), apiKey, token); return RequestSender.send(request, MessageList.class); } /** * Mark a message as read, and return its metadata, including its body. * * @param messageId The identifier of the message to mark as read. * @return A corresponding Message instance. */ public Message messageRead(final @NonNull String messageId) { return messageRead(messageId, true); } /** * Mark a message as read, and return its metadata, optionally including its * body. * * @param messageId The identifier of the message to mark as read. * @param withBody true if the message body should be included, * false otherwise. * @return A corresponding Message instance. */ public Message messageRead(final @NonNull String messageId, final boolean withBody) { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("message") .appendPath(messageId) .appendQueryParameter("with_body", Boolean.toString(withBody)) .build(), apiKey, token); return RequestSender.send(request, Message.class); } /** * Delete the specified message. * * @param messageId The identifier of the message to delete. */ public void messageDelete(final @NonNull String messageId) { final Request request = RequestFactory.delete( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("message") .appendPath(messageId) .build(), apiKey, token); RequestSender.send(request); } }