package com.facebook.unity;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.util.Base64;
import android.content.pm.*;
import android.content.pm.PackageManager.NameNotFoundException;
import com.facebook.*;
import com.facebook.Session.Builder;
import com.facebook.Session.OpenRequest;
import com.facebook.Session.StatusCallback;
import com.facebook.model.*;
import com.facebook.widget.WebDialog;
import com.facebook.widget.WebDialog.OnCompleteListener;
import com.unity3d.player.UnityPlayer;
public class FB {
private static final String TAG = "FBUnitySDK";
// i.e. the game object that receives this message
private static final String FB_UNITY_OBJECT = "UnityFacebookSDKPlugin";
private static Session session;
private static Intent intent;
private static AppEventsLogger appEventsLogger;
// if we have a session it has been opened.
private static void setSession(Session session) {
FB.session = session;
}
private static AppEventsLogger getAppEventsLogger() {
if (appEventsLogger == null) {
appEventsLogger = AppEventsLogger.newLogger(getActivity().getApplicationContext());
}
return appEventsLogger;
}
public static class UnityMessage {
private String methodName;
private Map<String, Serializable> params = new HashMap<String, Serializable>();
public UnityMessage(String methodName) {
this.methodName = methodName;
}
public UnityMessage put(String name, Serializable value) {
params.put(name, value);
return this;
}
public UnityMessage putCancelled() {
put("cancelled", true);
return this;
}
public UnityMessage putID(String id) {
put("id", id);
return this;
}
public void sendNotLoggedInError() {
sendError("not logged in");
}
public void sendError(String errorMsg) {
this.put("error", errorMsg);
send();
}
public void send() {
assert methodName != null : "no method specified";
String message = new JSONObject(this.params).toString();
Log.v(TAG,"sending to Unity "+this.methodName+"("+message+")");
UnityPlayer.UnitySendMessage(FB_UNITY_OBJECT, this.methodName, message);
}
}
private static boolean isLoggedIn() {
return Session.getActiveSession() != null && Session.getActiveSession().isOpened();
}
private static Activity getActivity() {
return UnityPlayer.currentActivity;
}
private static void initAndLogin(String params, final boolean show_login_dialog) {
Session session = (FB.isLoggedIn()) ? Session.getActiveSession() : new Builder(getActivity()).build();
final UnityMessage unityMessage = new UnityMessage((show_login_dialog) ? "OnLoginComplete" : "OnInitComplete");
// add the key hash to the JSON dictionary
// unityMessage.put("key_hash", "test_key_and");
unityMessage.put("key_hash", getKeyHash());
// if we have a session and are init-ing, we can just return here.
if (!SessionState.CREATED_TOKEN_LOADED.equals(session.getState()) && !show_login_dialog) {
unityMessage.send();
return;
}
// parse and separate the permissions into read and publish permissions
String[] parts = null;
final JSONObject unity_params;
try {
unity_params = new JSONObject(params);
if (!unity_params.isNull("scope") && !unity_params.getString("scope").equals("")) {
parts = unity_params.getString("scope").split(",");
}
} catch (JSONException e) {
Log.d(TAG, "couldn't parse params: "+params);
return;
}
List<String> publishPermissions = new ArrayList<String>();
List<String> readPermissions = new ArrayList<String>();
if(parts != null && parts.length > 0) {
for(String s:parts) {
if(Session.isPublishPermission(s)) {
publishPermissions.add(s);
} else {
readPermissions.add((s));
}
}
}
boolean hasPublishPermissions = !publishPermissions.isEmpty();
if (session != Session.getActiveSession()) {
Session.setActiveSession(session);
}
// check to see if the readPermissions have been TOSed already
// we don't need to show the readPermissions dialog if they have all been TOSed even though it's a mix
// of permissions
boolean showMixedPermissionsFlow = hasPublishPermissions && !session.getPermissions().containsAll(readPermissions);
// if we're logging in and showing a mix of publish and read permission, we need to split up the dialogs
// first just show the read permissions, then call initAndLogin() with just the publish permissions
if (showMixedPermissionsFlow) {
String publish_permissions = TextUtils.join(",", publishPermissions.toArray());
try {
unity_params.put("scope", publish_permissions);
} catch (JSONException e) {
// should never happen
Log.d(TAG, "couldn't add back the publish permissions " + publish_permissions);
return;
}
final String only_publish_params = unity_params.toString();
Session.StatusCallback afterReadPermissionCallback = new Session.StatusCallback() {
// callback when session changes state
@Override
public void call(Session session, SessionState state, Exception exception) {
if (!session.isOpened() && state != SessionState.CLOSED_LOGIN_FAILED) {
return;
}
session.removeCallback(this); // without this, the callback will loop infinitely
// if someone cancels on the read permissions and we don't even have the most basic access_token
// for basic info, we shouldn't be asking for publish permissions. It doesn't make sense
// and it simply won't work anyways.
if (session.getAccessToken() == null || session.getAccessToken().equals("")) {
unityMessage.putCancelled();
unityMessage.send();
return;
}
initAndLogin(only_publish_params, show_login_dialog);
}
};
if (session.isOpened()) {
session.requestNewReadPermissions(getNewPermissionsRequest(session, afterReadPermissionCallback, readPermissions));
} else {
session.openForRead(getOpenRequest(afterReadPermissionCallback, readPermissions));
}
return;
}
Session.StatusCallback finalCallback = new Session.StatusCallback() {
// callback when session changes state
@Override
public void call(Session session, SessionState state, Exception exception) {
if (!session.isOpened() && state != SessionState.CLOSED_LOGIN_FAILED) {
return;
}
session.removeCallback(this);
if (session.isOpened()) {
unityMessage.put("opened", true);
} else if (state == SessionState.CLOSED_LOGIN_FAILED) {
unityMessage.putCancelled();
}
if (session.getAccessToken() == null || session.getAccessToken().equals("")) {
unityMessage.send();
return;
}
// there's a chance a subset of the permissions were allowed even if the login was cancelled
// if the access token is there, try to get it anyways
FB.setSession(session);
unityMessage.put("access_token", session.getAccessToken());
Request.executeMeRequestAsync(session, new Request.GraphUserCallback() {
@Override
public void onCompleted(GraphUser user, Response response) {
if (user != null) {
unityMessage.put("user_id", user.getId());
}
unityMessage.send();
}
});
}
};
if (session.isOpened()) {
Session.NewPermissionsRequest req = getNewPermissionsRequest(session, finalCallback, (hasPublishPermissions) ? publishPermissions : readPermissions);
if (hasPublishPermissions) {
session.requestNewPublishPermissions(req);
} else {
session.requestNewReadPermissions(req);
}
} else {
OpenRequest req = getOpenRequest(finalCallback, (hasPublishPermissions) ? publishPermissions : readPermissions);
if (hasPublishPermissions) {
session.openForPublish(req);
} else {
session.openForRead(req);
}
}
}
private static OpenRequest getOpenRequest(StatusCallback callback, List<String> permissions) {
OpenRequest req = new OpenRequest(getActivity());
req.setCallback(callback);
req.setPermissions(permissions);
req.setDefaultAudience(SessionDefaultAudience.FRIENDS);
return req;
}
private static Session.NewPermissionsRequest getNewPermissionsRequest(Session session, StatusCallback callback, List<String> permissions) {
Session.NewPermissionsRequest req = new Session.NewPermissionsRequest(getActivity(), permissions);
req.setCallback(callback);
// This should really be "req.setCallback(callback);"
// Unfortunately the current underlying SDK won't add the callback when you do it that way
// TODO: when upgrading to the latest see if this can be "req.setCallback(callback);"
// if it still doesn't have it, file a bug!
session.addCallback(callback);
req.setDefaultAudience(SessionDefaultAudience.FRIENDS);
return req;
}
@UnityCallable
public static void Init(String params) {
FB.intent = getActivity().getIntent();
// tries to log the user in if they've already TOS'd the app
initAndLogin(params, /*show_login_dialog=*/false);
}
@UnityCallable
public static void Login(String params) {
initAndLogin(params, /*show_login_dialog=*/true);
}
@UnityCallable
public static void Logout(String params) {
Session.getActiveSession().closeAndClearTokenInformation();
new UnityMessage("OnLogoutComplete").send();
}
@UnityCallable
public static void AppRequest(String params_str) {
Log.v(TAG, "sendRequestDialog(" + params_str + ")");
final Bundle params = new Bundle();
final UnityMessage response = new UnityMessage("OnAppRequestsComplete");
if (!isLoggedIn()) {
response.sendNotLoggedInError();
return;
}
final JSONObject unity_params;
try {
unity_params = new JSONObject(params_str);
if (!unity_params.isNull("callback_id")) {
response.put("callback_id", unity_params.getString("callback_id"));
}
} catch (JSONException e) {
response.sendError("couldn't parse params: "+params_str);
return;
}
Iterator<?> keys = unity_params.keys();
while(keys.hasNext()) {
String key = (String)keys.next();
try {
if (key.equals("callback_id")) {
continue;
}
String value = unity_params.getString(key);
if (value != null) {
params.putString(key, value);
}
} catch (JSONException e) {
response.sendError("error getting value for key "+key+": "+e.toString());
return;
}
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
WebDialog requestsDialog = (
new WebDialog.RequestsDialogBuilder(getActivity(),
Session.getActiveSession(),
params))
.setOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(Bundle values,
FacebookException error) {
if (error != null) {
if(error.toString().equals("com.facebook.FacebookOperationCanceledException")) {
response.putCancelled();
response.send();
} else {
response.sendError(error.toString());
}
} else {
if(values != null) {
final String requestId = values.getString("request");
if(requestId == null) {
response.putCancelled();
}
}
for (String key : values.keySet()) {
response.put(key, values.getString(key));
}
response.send();
}
}
})
.build();
requestsDialog.show();
}
});
}
@UnityCallable
public static void FeedRequest(String params_str) {
Log.v(TAG, "FeedRequest(" + params_str + ")");
final UnityMessage response = new UnityMessage("OnFeedRequestComplete");
final JSONObject unity_params;
try {
unity_params = new JSONObject(params_str);
} catch (JSONException e) {
response.sendError("couldn't parse params: "+params_str);
return;
}
if (!isLoggedIn()) {
response.sendNotLoggedInError();
return;
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Bundle params = new Bundle();
Iterator<?> keys = unity_params.keys();
while (keys.hasNext()) {
String key = (String) keys.next();
try {
String value = unity_params.getString(key);
if (value != null) {
params.putString(key, value);
}
} catch (JSONException e) {
response.sendError("error getting value for key " + key + ": " + e.toString());
return;
}
}
WebDialog feedDialog = (
new WebDialog.FeedDialogBuilder(getActivity(),
Session.getActiveSession(),
params))
.setOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(Bundle values,
FacebookException error) {
// response
if (error == null) {
final String postID = values.getString("post_id");
if (postID != null) {
response.putID(postID);
} else {
response.putCancelled();
}
response.send();
} else if (error instanceof FacebookOperationCanceledException) {
// User clicked the "x" button
response.putCancelled();
response.send();
} else {
// Generic, ex: network error
response.sendError(error.toString());
}
}
})
.build();
feedDialog.show();
}
});
}
@UnityCallable
public static void PublishInstall(String params_str) {
final UnityMessage unityMessage = new UnityMessage("OnPublishInstallComplete");
final JSONObject unity_params;
try {
unity_params = new JSONObject(params_str);
if (!unity_params.isNull("callback_id")) {
unityMessage.put("callback_id", unity_params.getString("callback_id"));
}
Settings.publishInstallAsync(getActivity().getApplicationContext(), unity_params.getString("app_id"), new Request.Callback() {
@Override
public void onCompleted(Response response) {
if(response.getError() != null) {
unityMessage.sendError(response.getError().toString());
} else {
unityMessage.send();
}
}
});
} catch (JSONException e) {
unityMessage.sendError("couldn't parse params: " + params_str);
return;
}
}
@UnityCallable
public static void GetDeepLink(String params_str) {
final UnityMessage unityMessage = new UnityMessage("OnGetDeepLinkComplete");
Uri targetUri = intent.getData();
if (targetUri != null) {
unityMessage.put("deep_link", targetUri.toString());
} else {
unityMessage.put("deep_link", "");
}
unityMessage.send();
}
public static void SetIntent(Intent intent) {
FB.intent = intent;
GetDeepLink("");
}
public static void SetLimitEventUsage(String params) {
AppEventsLogger.setLimitEventUsage(getActivity().getApplicationContext(), Boolean.valueOf(params));
}
@UnityCallable
public static void AppEvent(String params) {
Log.v(TAG, "AppEvent(" + params + ")");
JSONObject unity_params;
try {
unity_params = new JSONObject(params);
Bundle parameters = new Bundle();
if (!unity_params.isNull("parameters")) {
JSONObject unity_params_parameter = unity_params.getJSONObject("parameters");
for (Iterator<?> keys = unity_params_parameter.keys(); keys.hasNext();) {
String key = (String) keys.next();
parameters.putString(key,unity_params_parameter.getString(key));
}
}
if (!unity_params.isNull("logPurchase")) {
FB.getAppEventsLogger().logPurchase(
new BigDecimal(unity_params.getDouble("logPurchase")),
Currency.getInstance(unity_params.getString("currency")),
parameters
);
} else if (!unity_params.isNull("logEvent")) {
FB.getAppEventsLogger().logEvent(
unity_params.getString("logEvent"),
unity_params.optDouble("valueToSum"),
parameters
);
} else {
Log.e(TAG, "couldn't logPurchase or logEvent params: "+params);
}
} catch (JSONException e) {
Log.e(TAG, "couldn't parse params: "+params);
return;
}
}
/**
* Provides the key hash to solve the openSSL issue with Amazon
* @return key hash
*/
private static String getKeyHash() {
try {
PackageInfo info = getActivity().getPackageManager().getPackageInfo(
getActivity().getPackageName(), PackageManager.GET_SIGNATURES);
for (Signature signature : info.signatures){
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(signature.toByteArray());
String keyHash = Base64.encodeToString(md.digest(), Base64.DEFAULT);
Log.d(TAG, "KeyHash: " + keyHash);
return keyHash;
}
} catch (NameNotFoundException e) {
} catch (NoSuchAlgorithmException e) {
}
return "";
}
}