/* * 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.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.Bitmap; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.LinearLayout; import com.pnikosis.materialishprogress.ProgressWheel; import com.squareup.okhttp.Request; import io.gameup.android.entity.Game; import io.gameup.android.entity.Leaderboard; import io.gameup.android.entity.LeaderboardList; import io.gameup.android.entity.Ping; import io.gameup.android.entity.Server; import io.gameup.android.http.RequestFactory; import io.gameup.android.entity.AchievementList; import io.gameup.android.http.RequestSender; import io.gameup.android.json.GsonFactory; import io.gameup.android.json.LoginAnonymousRequest; import io.gameup.android.json.LoginOAuthRequest; import io.gameup.android.json.LoginResponse; import io.gameup.android.push.Push; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.NonNull; /** * Static methods for interacting with the GameUp service without a gamer login. */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class GameUp { /** The API key to use when making requests. */ private static String apiKey = ""; /** Connection scheme and server addresses. */ public static final String SCHEME = "https"; public static final String ACCOUNTS_SERVER = "accounts.gameup.io"; public static final String API_SERVER = "api.gameup.io"; /** Login success and error page locations. */ public static final String ACCOUNTS_SUCCESS_PREFIX = SCHEME + "://" + ACCOUNTS_SERVER + "/v0/gamer/login/success"; public static final String ACCOUNTS_ERROR_PREFIX = SCHEME + "://" + ACCOUNTS_SERVER + "/v0/error"; /** User Agent string to be used in API calls and the login WebView. */ public static final String _WEBVIEW_USER_AGENT_ = "_WEBVIEW_USER_AGENT_"; public static final String USER_AGENT_TEMPLATE = "gameup-android-sdk/" + BuildConfig.VERSION_NAME + " (Android " + Build.VERSION.SDK_INT + "; " + _WEBVIEW_USER_AGENT_ + ")"; /** * Use the given API key for requests, until a different key is set. * * @param context The context calling the initialization, should usually be * the application's root context. * @param newApiKey The API key to use. */ public static void init(final @NonNull Activity context, final @NonNull String newApiKey) { init(context, newApiKey, null); } /** * Use the given API key for requests, until a different key is set. * * @param context The context calling the initialization, should usually be * the application's root context. * @param newApiKey The API key to use. * @param handler A callback triggered when push notifications are opened * by the gamer. */ public static void init(final @NonNull Activity context, final @NonNull String newApiKey, final Push.NotificationOpenedHandler handler) { apiKey = newApiKey; Push.init(context, handler); } /** * 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 static void ping() { final Request request = RequestFactory.head( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .build(), apiKey); 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/or token combination. * * Throws a GameUpException otherwise. * * @return A Ping object containing the server time. */ public static Ping pingGet() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .build(), apiKey); return RequestSender.send(request, Ping.class); } /** * Retrieve GameUp global service and/or server instance data. * * @return A Server instance containing GameUp service and/or server * instance information, for example current time. */ public static Server server() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("server") .build(), apiKey); return RequestSender.send(request, Server.class); } /** * Retrieve information about the game the given API key corresponds to, as * configured in the remote service. * * @return An entity representing remotely configured data about the game. */ public static Game game() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("game") .build(), apiKey); return RequestSender.send(request, Game.class); } /** * Get a list of achievements available for the game, excluding any gamer * data such as progress or completed timestamps. * * @return An AchievementList, containing Achievement instances, may be * empty if none are returned for the current game. */ public static AchievementList achievement() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("game") .appendPath("achievement") .build(), apiKey); return RequestSender.send(request, AchievementList.class); } /** * Request leaderboard metadata for all leaderboards available in the game. * * @return A LeaderboardList instance. Leaderboards in this response do not * contain entries. */ public static LeaderboardList leaderboard() { final Request request = RequestFactory.get( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("game") .appendPath("leaderboard") .build(), apiKey); return RequestSender.send(request, LeaderboardList.class); } /** * Request leaderboard metadata and the current top ranked gamers on a * specified leaderboard. * * @param leaderboardId The private ID of the leaderboard to request. * @return A Leaderboard instance. */ public static Leaderboard leaderboard(final @NonNull String leaderboardId) { return leaderboard(leaderboardId, 50, 0, false); } /** * Request leaderboard metadata and a page of entries, identified by the * limit and offset parameters provided. * * @param leaderboardId The private ID of the leaderboard to request. * @param limit The maximum number of 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 Leaderboard instance. */ public static Leaderboard 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("game") .appendPath("leaderboard") .appendPath(leaderboardId) .appendQueryParameter("limit", Integer.toString(limit)) .appendQueryParameter("offset", Integer.toString(offset)) .appendQueryParameter("with_scoretags", Boolean.toString(withScoretags)) .build(), apiKey); return RequestSender.send(request, Leaderboard.class); } /** * Execute the given script, without a gamer identification. * * 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 static void script(final @NonNull String scriptId) { scriptNoReturn(scriptId, null, ""); } /** * Execute the given script, without a gamer identification. * * 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 static void script(final @NonNull String scriptId, final @NonNull Object data) { scriptNoReturn(scriptId, data, ""); } static void scriptNoReturn(final @NonNull String scriptId, final @NonNull Object data, final @NonNull String token) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("game") .appendPath("script") .appendPath(scriptId) .build(), apiKey, token, data == null ? "" : GsonFactory.get().toJson(data)); RequestSender.send(request); } /** * Execute the given script, without a gamer identification. * * 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 static <T> T script(final @NonNull String scriptId, final @NonNull Class<T> type) { return scriptWithReturn(scriptId, type, null, ""); } /** * Execute the given script, without a gamer identification. * * 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 static <T> T script(final @NonNull String scriptId, final @NonNull Class<T> type, final @NonNull Object data) { return scriptWithReturn(scriptId, type, data, ""); } static <T> T scriptWithReturn(final @NonNull String scriptId, final @NonNull Class<T> type, final @NonNull Object data, final @NonNull String token) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.API_SERVER) .appendPath("v0") .appendPath("game") .appendPath("script") .appendPath(scriptId) .build(), apiKey, token, data == null ? "" : GsonFactory.get().toJson(data)); return RequestSender.send(request, type); } /** * Perform an anonymous login. * * @return A valid GameUpSession instance. */ public static GameUpSession loginAnonymous(final @NonNull String id) { final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.ACCOUNTS_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("login") .appendPath("anonymous") .build(), apiKey, GsonFactory.get().toJson(new LoginAnonymousRequest(id))); final LoginResponse loginResponse = RequestSender.send(request, LoginResponse.class); return new GameUpSession(apiKey, loginResponse.getToken()); } /** * Perform OAuth passthrough login for Facebook. * * @param accessToken The Facebook access token to send to GameUp. * @return A valid GameUpSession instance. */ public static GameUpSession loginOAuthFacebook( final @NonNull String accessToken) { return loginOAuth("facebook", accessToken, null); } /** * Perform OAuth passthrough login for Facebook. * * @param accessToken The Facebook access token to send to GameUp. * @param gameUpSession A session pointing to an existing account, on * successful login the new social profile will be * bound to this same account if possible, data will * be migrated from the given account to the new one * otherwise. * @return A valid GameUpSession instance. */ public static GameUpSession loginOAuthFacebook( final @NonNull String accessToken, final @NonNull GameUpSession gameUpSession) { return loginOAuth("facebook", accessToken, gameUpSession); } /** * Perform OAuth passthrough login for Google. * * @param accessToken The Google access token to send to GameUp. * @return A valid GameUpSession instance. */ public static GameUpSession loginOAuthGoogle( final @NonNull String accessToken) { return loginOAuth("google", accessToken, null); } /** * Perform OAuth passthrough login for Google. * * @param accessToken The Google access token to send to GameUp. * @param gameUpSession A session pointing to an existing account, on * successful login the new social profile will be * bound to this same account if possible, data will * be migrated from the given account to the new one * @return A valid GameUpSession instance. */ public static GameUpSession loginOAuthGoogle( final @NonNull String accessToken, final @NonNull GameUpSession gameUpSession) { return loginOAuth("google", accessToken, gameUpSession); } /** Internal OAuth passthrough login helper. */ private static GameUpSession loginOAuth(final @NonNull String type, final @NonNull String accessToken, final GameUpSession gameUpSession) { final String token = gameUpSession == null ? "" : gameUpSession.getToken(); final Request request = RequestFactory.post( new Uri.Builder().scheme(GameUp.SCHEME) .encodedAuthority(GameUp.ACCOUNTS_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("login") .appendPath("oauth2") .build(), apiKey, token, GsonFactory.get().toJson( new LoginOAuthRequest(type, accessToken))); final LoginResponse loginResponse = RequestSender.send(request, LoginResponse.class); return new GameUpSession(apiKey, loginResponse.getToken()); } /** * Pop up a Facebook login screen. * * @param context Used as a parent for the dialog and to look up resources. * @param listener The listener entity to be notified of login events. */ public static void loginFacebook(final @NonNull Context context, final @NonNull GameUpLoginListener listener) { login("facebook", context, listener, null); } /** * Pop up a Facebook login screen. * * @param context Used as a parent for the dialog and to look up resources. * @param listener The listener entity to be notified of login events. * @param gameUpSession A session pointing to an existing account, on * successful login the new social profile will be * bound to this same account if possible, data will * be migrated from the given account to the new one. */ public static void loginFacebook(final @NonNull Context context, final @NonNull GameUpLoginListener listener, final @NonNull GameUpSession gameUpSession) { login("facebook", context, listener, gameUpSession); } /** * Pop up a Google login screen. * * @param context Used as a parent for the dialog and to look up resources. * @param listener The listener entity to be notified of login events. */ public static void loginGoogle(final @NonNull Context context, final @NonNull GameUpLoginListener listener) { login("google", context, listener, null); } /** * Pop up a Google login screen. * * @param context Used as a parent for the dialog and to look up resources. * @param listener The listener entity to be notified of login events. * @param gameUpSession A session pointing to an existing account, on * successful login the new social profile will be * bound to this same account if possible, data will * be migrated from the given account to the new one. */ public static void loginGoogle(final @NonNull Context context, final @NonNull GameUpLoginListener listener, final @NonNull GameUpSession gameUpSession) { login("google", context, listener, gameUpSession); } /** * Pop up a Twitter login screen. * * @param context Used as a parent for the dialog and to look up resources. * @param listener The listener entity to be notified of login events. */ public static void loginTwitter(final @NonNull Context context, final @NonNull GameUpLoginListener listener) { login("twitter", context, listener, null); } /** * Pop up a Twitter login screen. * * @param context Used as a parent for the dialog and to look up resources. * @param listener The listener entity to be notified of login events. * @param gameUpSession A session pointing to an existing account, on * successful login the new social profile will be * bound to this same account if possible, data will * be migrated from the given account to the new one. */ public static void loginTwitter(final @NonNull Context context, final @NonNull GameUpLoginListener listener, final @NonNull GameUpSession gameUpSession) { login("twitter", context, listener, gameUpSession); } /** * Pop up a GameUp login screen. * * @param context Used as a parent for the dialog and to look up resources. * @param listener The listener entity to be notified of login events. */ public static void loginGameUp(final @NonNull Context context, final @NonNull GameUpLoginListener listener) { login("gameup", context, listener, null); } /** * Pop up a GameUp login screen. * * @param context Used as a parent for the dialog and to look up resources. * @param listener The listener entity to be notified of login events. * @param gameUpSession A session pointing to an existing account, on * successful login the new social profile will be * bound to this same account if possible, data will * be migrated from the given account to the new one */ public static void loginGameUp(final @NonNull Context context, final @NonNull GameUpLoginListener listener, final @NonNull GameUpSession gameUpSession) { login("gameup", context, listener, gameUpSession); } /** Internal login web view helper. */ @SuppressLint("SetJavaScriptEnabled") private static void login(final @NonNull String provider, final @NonNull Context context, final @NonNull GameUpLoginListener listener, final GameUpSession gameUpSession) { // Minimal dialog with no title. final Dialog dialog = new Dialog(context); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(final DialogInterface dialog) { listener.onLoginCancelled(); } }); // Indeterminate progress bar to indicate work being done in web view. final ProgressWheel progressWheel = new ProgressWheel(context); progressWheel.setBarColor(Color.BLUE); progressWheel.setBarWidth(12); progressWheel.setCircleRadius(100); progressWheel.setSpinSpeed(1.25f); progressWheel.spin(); // Web view to display remote login page. final WebView webView = new WebView(context); final WebSettings webSettings = webView.getSettings(); // JS required for Google login, and for tap to retry on network errors. webSettings.setJavaScriptEnabled(true); webSettings.setUserAgentString( USER_AGENT_TEMPLATE.replace( _WEBVIEW_USER_AGENT_, webSettings.getUserAgentString())); webView.setWebViewClient(new WebViewClient() { @Override public void onPageStarted(final WebView view, final String url, final Bitmap favicon) { super.onPageStarted(view, url, favicon); progressWheel.setVisibility(View.VISIBLE); if (url.startsWith(ACCOUNTS_SUCCESS_PREFIX)) { final Uri uri = Uri.parse(url); final String token = uri.getQueryParameter("token"); dialog.dismiss(); listener.onLoginComplete(new GameUpSession(apiKey, token)); } else if (url.startsWith(ACCOUNTS_ERROR_PREFIX)) { final Uri uri = Uri.parse(url); int status; try { status = Integer.valueOf(uri.getQueryParameter("status")); } catch (final Exception e) { status = 400; } final String message = uri.getQueryParameter("message"); dialog.dismiss(); listener.onLoginError(new GameUpException(status, message)); } } @Override public void onPageFinished(final WebView view, final String url) { super.onPageFinished(view, url); // Ensure the progress wheel is visible for another 750ms. // This gives better feedback to the user in the case of instant // failures, where it may otherwise appear nothing has happened. progressWheel.postDelayed(new Runnable() { public void run() { progressWheel.setVisibility(View.GONE); } }, 750); } @Override public void onReceivedError(final WebView view, final int errorCode, final String description, final String failingUrl) { webView.stopLoading(); webView.loadData( context.getString( R.string.gameup_webview_error, failingUrl), "text/html", "UTF-8"); super.onReceivedError(view, errorCode, description, failingUrl); } }); // Progress bar will appear in the top-left corner, inside the web view. webView.addView(progressWheel); // A separator for the cancel button. final View separator = new View(context); separator.setBackgroundColor(Color.parseColor("#D0D0D0")); // The cancel button itself. final Button cancelButton = (Button) LayoutInflater.from(context) .inflate(R.layout.gameup_login_cancel_button, null); cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { dialog.cancel(); } }); // The holder layout. final LinearLayout layout = new LinearLayout(context); layout.setBackgroundColor(Color.parseColor("#FFFFFF")); layout.setOrientation(LinearLayout.VERTICAL); layout.addView(webView); layout.addView(separator); layout.addView(cancelButton); // Width, height (and optionally weight) of each view inside the layout. webView.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT, 1)); separator.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 2)); cancelButton.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); // Set the holder layout as the full view of the dialog. dialog.setContentView(layout); // Stretch the dialog to nearly full screen. final ViewGroup.LayoutParams lp = dialog.getWindow().getAttributes(); lp.width = WindowManager.LayoutParams.MATCH_PARENT; lp.height = WindowManager.LayoutParams.MATCH_PARENT; // Pop up the dialog and load the initial login page. final Uri.Builder loginUri = new Uri.Builder() .scheme(GameUp.SCHEME) .encodedAuthority(GameUp.ACCOUNTS_SERVER) .appendPath("v0") .appendPath("gamer") .appendPath("login") .appendPath(provider) .appendQueryParameter("apiKey", apiKey); if (gameUpSession != null) { loginUri.appendQueryParameter("token", gameUpSession.getToken()); } dialog.show(); webView.loadUrl(loginUri.build().toString()); } }