package com.tealeaf.plugin.plugins;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import com.tealeaf.logger;
import com.tealeaf.TeaLeaf;
import com.tealeaf.EventQueue;
import com.tealeaf.plugin.IPlugin;
import com.tealeaf.plugin.PluginManager;
import java.io.*;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
import java.net.URI;
import android.app.Activity;
import android.content.Intent;
import android.content.Context;
import android.util.Log;
import android.os.Bundle;
import android.content.pm.ActivityInfo;
import android.content.SharedPreferences;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.Date;
import java.io.StringWriter;
import java.io.PrintWriter;
import android.net.Uri;
import android.view.Window;
import android.view.WindowManager;
import com.facebook.*;
import com.facebook.internal.*;
import com.facebook.FacebookDialogException;
import com.facebook.FacebookException;
import com.facebook.FacebookOperationCanceledException;
import com.facebook.FacebookAuthorizationException;
import com.facebook.FacebookRequestError;
import com.facebook.FacebookServiceException;
import com.facebook.login.*;
import com.facebook.share.model.*;
import com.facebook.share.widget.*;
import com.facebook.messenger.MessengerUtils;
import com.facebook.messenger.MessengerThreadParams;
import com.facebook.messenger.ShareToMessengerParams;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.FileOutputStream;
import java.io.IOException;
import android.util.Base64;
import android.location.Location;
import android.net.Uri;
import android.os.Environment;
import java.io.File;
import java.util.Set;
import java.security.Signature;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class FacebookPlugin implements IPlugin {
Context _context;
private Activity _activity;
Integer activeRequest = null;
String _facebookAppID = "";
String _facebookDisplayName = "";
private String appID = null;
private String userID = null;
public static final int INVALID_ERROR = -2;
private int REQUEST_CODE_SHARE_TO_MESSENGER = 1;
private String _tempFilename = "fbm-temp-share.png";
private String _sharedImagePath = null;
private CallbackManager callbackManager;
private AccessTokenTracker accessTokenTracker;
private GameRequestDialog requestDialog;
private ShareDialog shareDialog;
void onJSONException (JSONException e) {
logger.log("{facebook} JSONException:", e.getMessage());
}
// ---------------------------------------------------------------------------
// JavaScript interface
// ---------------------------------------------------------------------------
public void facebookInit (String s_json) {
logger.log("{facebook} init");
JSONObject res;
try {
res = new JSONObject(s_json);
} catch (JSONException e) {
onJSONException(e);
return;
}
logger.log("\t", res);
appID = res.optString("appId", "");
}
public void getLoginStatus(String s_opts, Integer requestId) {
logger.log("{facebook} getLoginStatus");
sendResponse(getResponse(), null, requestId);
}
public void getAuthResponse(String s_opts, Integer requestId) {
logger.log("{facebook} getAuthResponse");
JSONObject authResponse = getResponse();
JSONObject response = new JSONObject();
try {
response.put("authResponse", authResponse);
} catch (JSONException e) {
// What could possibly cause this to throw an error?
}
sendResponse(response, null, requestId);
}
public void login(String s_opts, Integer requestId) {
activeRequest = requestId;
logger.log("{facebook} login");
String errorMessage;
JSONObject opts;
try {
opts = new JSONObject(s_opts);
} catch (JSONException e) {
onJSONException(e);
sendResponse(getErrorResponse("error parsing json opts"), requestId);
return;
}
String scopes = opts.optString("scope", "");
// Get the permissions
String[] arrayPermissions = scopes.split(",");
List<String> permissions = null;
if (arrayPermissions.length > 0) {
permissions = Arrays.asList(arrayPermissions);
}
// Check if already logged in
if (isLoggedIn()) {
// Reauthorize flow
boolean publishPermissions = false;
boolean readPermissions = false;
// Figure out if this will be a read or publish reauthorize
if (permissions == null) {
// No permissions, read
readPermissions = true;
}
// Loop through the permissions to see what is being requested
for (String permission : arrayPermissions) {
if (isPublishPermission(permission)) {
publishPermissions = true;
} else {
readPermissions = true;
}
// Break if we have a mixed bag, as this is an error
if (publishPermissions && readPermissions) {
break;
}
}
if (publishPermissions && readPermissions) {
try {
JSONObject err = new JSONObject();
err.put("error", "Cannot ask for both read and publish permissions.");
sendResponse(err, null, requestId);
} catch (JSONException e) {
onJSONException(e);
}
activeRequest = null;
return;
} else {
// Check for write permissions, the default is read (empty)
if (publishPermissions) {
// Request new publish permissions
LoginManager.getInstance().logInWithPublishPermissions(
_activity,
permissions
);
} else {
// Request new read permissions
LoginManager.getInstance().logInWithReadPermissions(
_activity,
permissions
);
}
}
} else {
// TODO: is this separation needed in v4?
LoginManager.getInstance().logInWithReadPermissions(
_activity,
permissions
);
}
}
public void api (String s_json, Integer _requestId) {
final Integer requestId = _requestId;
log("api");
JSONObject opts;
JSONObject _params;
String _method;
String path;
try {
opts = new JSONObject(s_json);
_params = opts.getJSONObject("params");
_method = opts.optString("method", "get");
path = opts.getString("path");
} catch (JSONException e) {
onJSONException(e);
sendResponse(getErrorResponse("error parsing JSON opts"), requestId);
return;
}
Bundle params = null;
if (_params.length() > 0) {
try {
params = BundleJSONConverter.convertToBundle(_params);
} catch (JSONException e) {
log("api - error converting JSONObject to Bundle");
}
}
HttpMethod method;
if (_method.equals("post")) {
method = HttpMethod.POST;
} else if (_method.equals("delete")) {
method = HttpMethod.DELETE;
} else {
method = HttpMethod.GET;
}
if (path.charAt(0) == '/') {
path = (new StringBuilder(path)).deleteCharAt(0).toString();
}
GraphRequest req = new GraphRequest(AccessToken.getCurrentAccessToken(), path, params, method, new GraphRequest.Callback() {
@Override
public void onCompleted(GraphResponse res) {
if (res.getError() != null) {
sendResponse(getFacebookRequestError(res.getError()), null, requestId);
} else {
sendResponse(res.getRawResponse(), null, requestId);
}
}
});
req.executeAndWait();
}
public void ui(String s_json, Integer requestId) {
log("ui");
final Integer _requestId = requestId;
JSONObject json = null;
Bundle params = null;
// Parse JSON;
try {
json = new JSONObject(s_json);
} catch (JSONException e) {
onJSONException(e);
log("ui failed to parse JSON blob");
sendResponse(getErrorResponse("failed to parse JSON"), null, requestId);
return;
}
if (json.length() > 0) {
// get json bundle
try {
params = BundleJSONConverter.convertToBundle(json);
} catch (JSONException e) {
log("ui failed to convert JSON to bundle");
onJSONException(e);
sendResponse(
getErrorResponse("error converting JSONObject to bundle"),
null,
_requestId
);
return;
}
}
final String method = params.getString("method");
if (method == null) {
log("ui failed - method param is required");
sendResponse(getErrorResponse("method param is required"), null, requestId);
return;
}
params.remove("method");
final Bundle dialogParams = params;
final Activity devkitActivity = _activity;
if (method.equalsIgnoreCase("feed")) {
// TODO: build share content?
} else if (method.equalsIgnoreCase("apprequests")) {
activeRequest = requestId;
ArrayList<String> filtersArray = dialogParams.getStringArrayList("filters");
String filtersString = "";
GameRequestContent.Filters filters = null;
if (filtersArray != null) {
filtersString = filtersArray.get(0);
if (filtersString.equalsIgnoreCase("app_non_users")) {
filters = GameRequestContent.Filters.APP_NON_USERS;
} else {
filters = GameRequestContent.Filters.APP_USERS;
}
}
String objectId = dialogParams.getString("objectId");
GameRequestContent.ActionType actionType = null;
String actionTypeString = dialogParams.getString("actionType");
if (actionTypeString != null) {
if (actionTypeString.equalsIgnoreCase("send")) {
actionType = GameRequestContent.ActionType.SEND;
} else if (actionTypeString.equalsIgnoreCase("askfor")) {
actionType = GameRequestContent.ActionType.ASKFOR;
} else if (actionTypeString.equalsIgnoreCase("turn")) {
actionType = GameRequestContent.ActionType.TURN;
} else {
log("error - unknown action type " + actionTypeString);
}
}
GameRequestContent.Builder builder = new GameRequestContent.Builder()
.setMessage(dialogParams.getString("message"))
.setTitle(dialogParams.getString("title"));
if (filters != null) {
builder.setFilters(filters);
}
if (actionType != null && objectId != null) {
builder
.setObjectId(objectId)
.setActionType(actionType);
}
String toString = dialogParams.getString("to");
if (toString != null) {
String[] toIds = toString.split(",");
builder.setTo(toIds[0]);
// warn if more than one specified
if (toIds.length > 1) {
log("warning - android facebook only supports sending " +
"messages to one user at a time. Other recipients skipped."
);
}
}
GameRequestContent content = builder.build();
requestDialog.show(content);
} else if (method.equalsIgnoreCase("share") || method.equalsIgnoreCase("share_open_graph")) {
// TODO: change so parameters match js api
String imageUrl = dialogParams.getString("imageUrl");
// setContentUrl doesnt exist, even though this is verbatim docs example
// can only submit images
/*
if (ShareDialog.canShow(ShareLinkContent.class)) {
ShareLinkContent linkContent = new ShareLinkContent.Builder()
.setContentTitle(dialogParams.getString("title"))
.setContentDescription(dialogParams.getString("description"));
.setContentUrl(Uri.parse(href))
.build();
shareDialog.show(linkContent);
}
*/
ShareLinkContent linkContent = new ShareLinkContent.Builder()
.setContentTitle(dialogParams.getString("title"))
.setContentDescription(dialogParams.getString("description"))
.setImageUrl(Uri.parse(imageUrl))
.build();
shareDialog.show(linkContent);
sendResponse(getResponse(), null, requestId);
} else {
sendResponse(getErrorResponse("unsupported method"), requestId);
}
}
public void logout(String json, Integer requestId) {
LoginManager.getInstance().logOut();
JSONObject res = new JSONObject();
try {
res.put("state", "closed");
} catch (JSONException e) {
onJSONException(e);
}
sendResponse(res, null, requestId);
}
// NOTE: facebook messenger does NOT support transparency in pngs
// from the sharing plugin
// from http://stackoverflow.com/a/17506538/1279574
public Bitmap bitmapFromBase64(String input) {
// TODO: check mime type
// assume png for now -- "data:image\/png;base64,....."
Integer commaIndex = input.indexOf(",");
String imageDataBytes = input;
if (commaIndex > 0) {
imageDataBytes = input.substring(commaIndex + 1);
}
byte[] decodedByte = Base64.decode(imageDataBytes, 0);
return BitmapFactory.decodeByteArray(decodedByte, 0, decodedByte.length);
}
// from http://stackoverflow.com/a/21590345/1279574
private Uri saveImageLocally(Bitmap bitmap) {
File outputDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
);
// create the same file over and over (and for every app)
File outputFile = new File(outputDir, _tempFilename);
Uri uri = null;
try {
FileOutputStream out = new FileOutputStream(outputFile);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
out.close();
// save file path and create uri for sharing
_sharedImagePath = outputFile.getAbsolutePath();
uri = Uri.parse(outputFile.toURI().toString());
} catch (Exception e) {
log("{fbm} exception writing bitmap: ", e);
return null;
}
log("saved bitmap in sharable location", uri);
return uri;
}
public void shareImage(String jsonData, final Integer requestId) {
log("share image requested");
String image = "";
Bitmap bitmap;
Uri uri = null;
boolean failed = true;
try {
JSONObject jsonObject = new JSONObject(jsonData);
if (jsonObject.has("image")) {
image = jsonObject.getString("image");
}
if (jsonObject.has("filename")) {
_tempFilename = jsonObject.getString("filename");
}
// write image to shareable path
if (image != "") {
_sharedImagePath = null;
uri = null;
log("creating bitmap from base 64 content");
bitmap = bitmapFromBase64(image);
if (bitmap != null) {
uri = saveImageLocally(bitmap);
log("saving image in shared location:", uri);
} else {
log("failed to create bitmap");
}
if (uri != null) {
log("sending image", uri);
String mimeType = "image/png";
// look for messenger
if (MessengerUtils.hasMessengerInstalled(_context)) {
ShareToMessengerParams shareToMessengerParams =
ShareToMessengerParams.newBuilder(uri, mimeType)
.build();
MessengerUtils.shareToMessenger(
_activity,
REQUEST_CODE_SHARE_TO_MESSENGER,
shareToMessengerParams
);
failed = false;
} else {
// fall back to regular image share
ShareLinkContent linkContent = new ShareLinkContent.Builder()
.setImageUrl(uri)
.build();
shareDialog.show(linkContent);
}
}
}
} catch (Exception e) {
log("Exception while sharing image", e);
e.printStackTrace();
}
sendResponse(new ShareCompletedEvent(!failed), null, requestId);
}
public class ShareCompletedEvent extends com.tealeaf.event.Event {
boolean completed;
public ShareCompletedEvent(boolean completed) {
super("ShareCompleted");
this.completed = completed;
}
}
// ---------------------------------------------------------------------------
// Facebook Interface Utilities
// ---------------------------------------------------------------------------
private static final Set<String> publishPermissionsSet = new HashSet<String>() {
{
add("publish_actions");
add("ads_management");
add("create_event");
add("rsvp_event");
}
};
private boolean isPublishPermission(String permission) {
return permission != null &&
(permission.startsWith("publish")) ||
(permission.startsWith("manage")) ||
publishPermissionsSet.contains(permission);
}
/**
* Check if user is logged in.
*
* @return boolean
*/
public boolean isLoggedIn() {
return AccessToken.getCurrentAccessToken() != null;
}
/**
* Create a Facebook Response object that matches the one for the Javascript
* SDK
*
* @return JSONObject - the response object
*/
public JSONObject getResponse() {
String response;
// TODO: go find the new js api
if (isLoggedIn()) {
AccessToken currentToken = AccessToken.getCurrentAccessToken();
Date today = new Date();
long expirationTime = currentToken.getExpires().getTime();
long expiresTimeInterval = (expirationTime - today.getTime()) / 1000L;
long expiresIn = (expiresTimeInterval > 0) ? expiresTimeInterval : 0;
// Make list of grantedScopes
final Set<String> permissions = currentToken.getPermissions();
StringBuilder sb = new StringBuilder();
String comma = ",";
for (String perm : permissions) {
sb.append(perm).append(comma);
}
sb.setLength(sb.length() - comma.length());
String grantedScopes = sb.toString();
response = "{"
+ "\"status\": \"connected\","
+ "\"authResponse\": {"
+ "\"accessToken\": \"" + currentToken + "\","
+ "\"expiresIn\": \"" + expiresIn + "\","
+ "\"sig\": \"...\","
+ "\"grantedScopes\": \"" + grantedScopes + "\","
+ "\"userID\": \"" + userID + "\""
+ "}"
+ "}";
} else {
response = "{"
+ "\"status\": \"unknown\""
+ "}";
}
try {
return new JSONObject(response);
} catch (JSONException e) {
e.printStackTrace();
}
return new JSONObject();
}
/**
* Create JSON object for facebook err request error
*
* @return JSONObject
*/
public JSONObject getFacebookRequestError(FacebookRequestError e) {
JSONObject err = new JSONObject();
JSONObject res = new JSONObject();
try {
err.put("code", e.getErrorCode());
err.put("type", e.getErrorType());
err.put("message", e.getErrorMessage());
res.put("error", err);
} catch (JSONException exception) {
onJSONException(exception);
}
return res;
}
/**
* Create JSON object to be returned to JS
*/
public JSONObject getErrorResponse(Exception err, String msg, int code) {
if (err instanceof FacebookServiceException) {
return getFacebookRequestError(
((FacebookServiceException) err).getRequestError()
);
}
if (err instanceof FacebookDialogException) {
code = ((FacebookDialogException) err).getErrorCode();
}
if (msg == null) {
msg = err.getMessage();
}
JSONObject jsonError = new JSONObject();
JSONObject ret = new JSONObject();
try {
jsonError.put("code", code);
jsonError.put("message", msg);
ret.put("error", jsonError);
} catch (JSONException e) {
onJSONException(e);
ret = new JSONObject();
}
return ret;
}
private JSONObject getErrorResponse(String msg) {
JSONObject response = new JSONObject();
try {
response.put("error", msg);
} catch (JSONException e) {
log("JSON exception while constructing error response");
}
return response;
}
private void onAccessTokenChange(AccessToken oldToken, AccessToken token) {
JSONObject payload = new JSONObject();
if (token != null && oldToken == null) {
// sendEvent("auth.login", payload);
sendEvent("auth.statusChange", payload);
} else if (token == null && oldToken != null) {
sendEvent("logout", payload);
sendEvent("auth.statusChange", payload);
} else {
sendEvent("auth.authResponseChanged", payload);
}
}
private static void sendEvent(String event, Object payload) {
String _payload = "";
if (payload instanceof JSONObject) {
_payload = payload.toString();
} else if (payload instanceof String) {
_payload = (String)payload;
} else {
log("sendEvent cannot coerce payload to string");
}
PluginManager.sendEvent(event, "FacebookPlugin", _payload);
}
private void sendResponse(Object res, String err, Integer requestId) {
if (activeRequest == requestId) { activeRequest = null; }
String response = "";
if (res instanceof JSONObject) {
response = res.toString();
} else if (res instanceof String) {
response = (String) res;
}
log("sendResponse", "requestId:", requestId);
if (requestId != null) {
PluginManager.sendResponse(response, err, requestId);
}
}
private void sendResponse(Object res, Integer requestId) {
sendResponse(res, null, requestId);
}
public static void log(Object... args) {
Object[] newArgs = new Object[args.length + 1];
newArgs[0] = "{facebook}";
System.arraycopy(args, 0, newArgs, 1, args.length);
logger.log(newArgs);
}
private void handleError(Exception e, Integer requestId) {
String msg;
int code = INVALID_ERROR;
if (e instanceof FacebookOperationCanceledException) {
// Send undefined
sendResponse(null, null, requestId);
} else {
if (e instanceof FacebookDialogException) {
msg = "Dialog exception: " + e.getMessage();
} else {
msg = e.getMessage();
}
JSONObject res = getErrorResponse(e, msg, code);
sendResponse(res, null, requestId);
}
}
// ---------------------------------------------------------------------------
// Plugin Interface Methods
// ---------------------------------------------------------------------------
public void onCreateApplication(Context applicationContext) {
_context = applicationContext;
}
public void onCreate(Activity activity, Bundle savedInstanceState) {
_activity = activity;
PackageManager manager = _activity.getPackageManager();
try {
Bundle meta = manager.getApplicationInfo(_activity.getPackageName(), PackageManager.GET_META_DATA).metaData;
if (meta != null) {
_facebookAppID = meta.get("FACEBOOK_APP_ID").toString();
_facebookDisplayName = meta.get("FACEBOOK_DISPLAY_NAME").toString();
}
FacebookSdk.sdkInitialize(_activity.getApplicationContext());
callbackManager = CallbackManager.Factory.create();
accessTokenTracker = new AccessTokenTracker() {
@Override
protected void onCurrentAccessTokenChanged(
AccessToken oldAccessToken,
AccessToken currentAccessToken) {
// App code
onAccessTokenChange(oldAccessToken, currentAccessToken);
}
};
LoginManager.getInstance().registerCallback(callbackManager,
new FacebookCallback<LoginResult>() {
@Override
public void onSuccess(LoginResult loginResult) {
log("facebook login response - success");
JSONObject response = getResponse();
sendEvent("auth.login", response);
// respond to login request
sendResponse(response, null, activeRequest);
}
@Override
public void onCancel() {
log("facebook login response - cancel");
// TODO: is this really the best way to say user cancelled?
handleError(new FacebookOperationCanceledException(), activeRequest);
}
@Override
public void onError(FacebookException exception) {
log("facebook login response - error");
handleError(exception, activeRequest);
}
});
requestDialog = new GameRequestDialog(_activity);
requestDialog.registerCallback(callbackManager, new FacebookCallback<GameRequestDialog.Result>() {
public void onSuccess(GameRequestDialog.Result result) {
log("game request result - success");
sendResponse("", null, activeRequest);
}
public void onCancel() {
log("game request result - success");
sendResponse("", "cancelled", activeRequest);
}
public void onError(FacebookException error) {
log("game request result - success");
sendResponse("", "error", activeRequest);
}
});
shareDialog = new ShareDialog(_activity);
JSONObject ready = new JSONObject();
try { ready.put("status", "OK"); } catch (JSONException e) {}
PluginManager.sendEvent("FacebookPluginReady", "FacebookPlugin", ready);
} catch (Exception e) {
log("{facebook} Exception on start:", e.getMessage());
}
// // generate facebook keyhash - uncomment when needed
// try {
// log("displaying keyhash for " + _activity.getPackageName());
// PackageInfo info = getPackageManager().getPackageInfo(
// // "com.facebook.samples.hellofacebook",
// "co.weeby.flappy",
// // _activity.getPackageName(),
// PackageManager.GET_SIGNATURES);
// for (Signature signature : info.signatures) {
// MessageDigest md = MessageDigest.getInstance("SHA");
// md.update(signature.toByteArray());
// Log.d("Facebook KeyHash:", Base64.encodeToString(md.digest(), Base64.DEFAULT));
// }
// } catch (NameNotFoundException e) {
// } catch (NoSuchAlgorithmException e) {
// }
}
public void onResume() {
}
public void onStart() {
}
public void onPause() {
}
public void onStop() {
}
public void onDestroy() {
accessTokenTracker.stopTracking();
}
public void onNewIntent(Intent intent) {
}
public void setInstallReferrer(String referrer) {
}
@Override
public void onActivityResult(Integer requestCode, Integer resultCode, Intent data) {
callbackManager.onActivityResult(requestCode, resultCode, data);
}
}