/*
* 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());
}
}