diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..5a6ae853f6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*~
+*.DS_Store
+*.class
+/facebook/bin/
\ No newline at end of file
diff --git a/facebook/.classpath b/facebook/.classpath
new file mode 100644
index 0000000000..c3c33d0530
--- /dev/null
+++ b/facebook/.classpath
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/facebook/.project b/facebook/.project
new file mode 100644
index 0000000000..debe3f69cf
--- /dev/null
+++ b/facebook/.project
@@ -0,0 +1,17 @@
+
+
+ FacebookSDK
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/facebook/.settings/org.eclipse.jdt.core.prefs b/facebook/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000000..7764660679
--- /dev/null
+++ b/facebook/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,12 @@
+#Fri May 07 12:11:08 PDT 2010
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/facebook/src/com/facebook/android/Facebook.java b/facebook/src/com/facebook/android/Facebook.java
new file mode 100644
index 0000000000..7d716d354f
--- /dev/null
+++ b/facebook/src/com/facebook/android/Facebook.java
@@ -0,0 +1,241 @@
+package com.facebook.android;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+
+import com.facebook.android.Util.Callback;
+
+// TODO(ssoneff):
+// make Facebook button
+// logout function?
+
+// size, title of uiActivity
+// support multiple facebook sessions? provide session manager?
+// wrapper for data: FacebookObject?
+// lower pri: auto uiInteraction on session failure?
+// request queue? request callbacks for loading, cancelled?
+
+// Questions:
+// fix fbconnect://...
+// oauth redirect not working
+// oauth does not return expires_in
+// expires_in is duration or expiration?
+// for errors: use string (message), error codes, or exceptions?
+// why callback on both receive response and loaded?
+// keep track of which permissions this session has?
+
+public class Facebook {
+
+ public static final String SUCCESS_URI = "fbconnect://success";
+ public static final String PREFS_KEY = "facebook-session";
+
+ private static final String OAUTH_ENDPOINT = "http://graph.dev.facebook.com/oauth/authorize";
+ private static final String UI_SERVER = "http://www.facebook.com/connect/uiserver.php";
+ private static final String GRAPH_BASE_URL = "https://graph.facebook.com/";
+ private static final String RESTSERVER_URL = "http://api.facebook.com/restserver.php";
+ private static final String TOKEN = "oauth_token";
+
+ private Context mContext;
+ private String mAppId;
+ private String mAccessToken = null;
+ private long mAccessExpires = 0;
+
+
+ // Initialization
+
+ public Facebook(Context c, String clientId) {
+ this.mContext = c;
+ this.mAppId = clientId;
+ }
+
+ public void login(DialogListener listener) {
+ authorize(null, listener);
+ }
+
+ public void logout() {
+ // TODO(ssoneff) how does logout work? de-auth api method??
+ // support multiple logout listeners?
+ }
+
+ public void authorize(String[] permissions, final DialogListener listener) {
+ Bundle params = new Bundle();
+ params.putString("display", "touch");
+ params.putString("type", "user_agent");
+ params.putString("client_id", mAppId);
+ params.putString("redirect_uri", SUCCESS_URI);
+ if (permissions != null) params.putString("scope", Util.join(permissions, ','));
+ dialog("login", params, new DialogListener() {
+
+ @Override
+ public void onDialogSucceed(Bundle values) {
+ String token = values.getString("access_token");
+ String expires = values.getString("expires_in");
+ Log.d("Facebook", "Success! access_token=" + token + " expires=" + expires);
+
+ SharedPreferences s = mContext.getSharedPreferences(Facebook.PREFS_KEY, Context.MODE_PRIVATE);
+ s.edit().putString("access_token", token);
+ s.edit().putString("expires_in", expires);
+
+ // WTF? commit does not work on emulator ... file system problem?
+ if (s.edit().commit()) {
+ Log.d("Facebook-WebView", "changes committed");
+ } else {
+ Log.d("Facebook-WebView", "changes NOT committed");
+ }
+ s = mContext.getSharedPreferences(Facebook.PREFS_KEY, Context.MODE_PRIVATE);
+ Log.d("Facebook-Callback", "Stored: access_token=" + s.getString("access_token", "NONE"));
+
+ setAccessToken(token);
+ setAccessExpiresIn(expires);
+ listener.onDialogSucceed(values);
+ }
+
+ @Override
+ public void onDialogFail(String error) {
+ Log.d("Facebook-Callback", "Dialog failed: " + error);
+ listener.onDialogFail(error);
+ }
+
+ @Override
+ public void onDialogCancel() {
+ Log.d("Facebook-Callback", "Dialog cancelled");
+ listener.onDialogCancel();
+ }
+ });
+ }
+
+ // API requests
+
+ // support old API: method provided as parameter
+ public void request(Bundle parameters, RequestListener listener) {
+ request(null, "GET", parameters, listener);
+ }
+
+ public void request(String graphPath, RequestListener listener) {
+ request(graphPath, "GET", new Bundle(), listener);
+ }
+
+ public void request(String graphPath, Bundle parameters, RequestListener listener) {
+ request(graphPath, "GET", parameters, listener);
+ }
+
+ public void request(String graphPath, String httpMethod, Bundle parameters, final RequestListener listener) {
+ if (isSessionValid()) { parameters.putString(TOKEN, mAccessToken); }
+ String url = graphPath != null ? GRAPH_BASE_URL + graphPath : RESTSERVER_URL;
+ Util.asyncOpenUrl(url, httpMethod, Util.encodeUrl(parameters), new Callback() {
+ public void call(String response) {
+ try {
+ JSONObject o = new JSONObject(response);
+ if (o.has("error")) {
+ listener.onRequestFail(o.getString("error"));
+ } else {
+ listener.onRequestSucceed(o);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ listener.onRequestFail(e.getMessage());
+ }
+ }
+ });
+ }
+
+
+ // UI Server requests
+
+ public void dialog(String action, DialogListener listener) {
+ dialog(action, null, listener);
+ }
+
+ public void dialog(String action, Bundle parameters, final DialogListener listener) {
+ // need logic to determine correct endpoint for resource, e.g. "login" --> "oauth/authorize"
+ String endpoint = action.equals("login") ? OAUTH_ENDPOINT : UI_SERVER;
+ final String url = parameters == null ? endpoint + "?" + Util.encodeUrl(parameters) : endpoint;
+
+ // This is buggy: webview dies with null pointer exception (but not in my code)...
+ /*
+ final ProgressDialog spinner = ProgressDialog.show(mContext, "Facebook", "Loading...");
+ final Handler h = new Handler();
+
+ // start async data fetch
+ Util.asyncOpenUrl(url, "GET", Util.encodeUrl(parameters), new Callback() {
+ @Override public void call(final String response) {
+ Log.d("Facebook", "got response: " + response);
+ if (response.length() == 0) listener.onDialogFail("Empty response");
+ h.post(new Runnable() {
+ @Override public void run() {
+ //callback: close progress dialog
+ spinner.dismiss();
+ new FbDialog(mContext, url, response, listener).show();
+ }
+ });
+ }
+ });
+ */
+ new FbDialog(mContext, url + "?" + Util.encodeUrl(parameters), "", listener).show();
+ }
+
+
+ // utilities
+
+ public boolean isSessionValid() {
+ if (mAccessToken == null) {
+ SharedPreferences store = mContext.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE);
+ mAccessToken = store.getString("access_token", null);
+ mAccessExpires = store.getLong("expires_in", 0);
+ // throw not logged-in exception if no access token? need to call login() first
+ }
+ Log.d("Facebook SDK", "session valid? token=" + mAccessToken + " duration: " + -(System.currentTimeMillis() - mAccessExpires));
+ return mAccessToken != null && (mAccessExpires == 0 || System.currentTimeMillis() < mAccessExpires);
+ }
+
+ // get/set
+
+ public String getAccessToken() {
+ return mAccessToken;
+ }
+
+ public long getAccessExpires() {
+ return mAccessExpires;
+ }
+
+ public void setAccessToken(String token) {
+ mAccessToken = token;
+ }
+
+ public void setAccessExpiresIn(String expires_in) {
+ if (expires_in != null) mAccessExpires = System.currentTimeMillis() + Integer.parseInt(expires_in)*1000;
+ }
+
+
+ // callback interfaces
+
+ public static abstract class SessionLogoutListener {
+
+ public void onSessionLogoutStart() { }
+
+ public void onSessionLogoutFinish() { }
+ }
+
+ public static abstract class RequestListener {
+
+ public abstract void onRequestSucceed(JSONObject response);
+
+ public abstract void onRequestFail(String error);
+ }
+
+ public static abstract class DialogListener {
+
+ public abstract void onDialogSucceed(Bundle values);
+
+ public abstract void onDialogFail(String error);
+
+ public void onDialogCancel() { }
+ }
+}
diff --git a/facebook/src/com/facebook/android/FbButton.java b/facebook/src/com/facebook/android/FbButton.java
new file mode 100644
index 0000000000..7f690b477a
--- /dev/null
+++ b/facebook/src/com/facebook/android/FbButton.java
@@ -0,0 +1,13 @@
+package com.facebook.android;
+
+import android.content.Context;
+import android.widget.ImageButton;
+
+public class FbButton extends ImageButton {
+
+ public FbButton(Context context) {
+ super(context);
+ // TODO Auto-generated constructor stub
+ }
+
+}
diff --git a/facebook/src/com/facebook/android/FbDialog.java b/facebook/src/com/facebook/android/FbDialog.java
new file mode 100644
index 0000000000..ec2fe68d20
--- /dev/null
+++ b/facebook/src/com/facebook/android/FbDialog.java
@@ -0,0 +1,82 @@
+/**
+ *
+ */
+package com.facebook.android;
+
+import com.facebook.android.Facebook.DialogListener;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.FrameLayout.LayoutParams;
+
+public class FbDialog extends Dialog {
+
+ private String mUrl;
+ private String mData;
+ private DialogListener mListener;
+ private WebView mWebView;
+
+ public FbDialog(Context context, String url, String data, DialogListener listener) {
+ super(context);
+ mUrl = url;
+ mData = data;
+ mListener = listener;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ initView();
+ }
+
+ private void initView() {
+ mWebView = new WebView(getContext());
+ mWebView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
+ mWebView.setWebViewClient(new FbDialog.FbWebViewClient());
+ WebSettings webSettings = mWebView.getSettings();
+ webSettings.setJavaScriptEnabled(true);
+ //mWebView.loadDataWithBaseURL(mUrl, mData, "text/html", "UTF-8", null); // BUG: null pointer somewhere
+ mWebView.loadUrl(mUrl);
+
+ // extract title and size from data
+ addContentView(mWebView, new LayoutParams(280, 360));
+ setTitle("Facebook Rulz");
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ mListener.onDialogCancel();
+ }
+
+ private class FbWebViewClient extends WebViewClient {
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ Log.d("Facebook-WebView", "Webview loading URL: " + url);
+ if (url.startsWith(Facebook.SUCCESS_URI)) {
+ mListener.onDialogSucceed(Util.parseUrl(url));
+ FbDialog.this.dismiss();
+ }
+ return false;
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ Log.d("Facebook-WebView", "Loaded URL: " + url);
+ // HACK HACK HACK: oauth needs to be fixed on server-side
+ if (url.contains("auth_token=")) {
+ mListener.onDialogSucceed(Util.parseUrl("http://success/#access_token=110862205611506%7Ce54333664a458cabe3ed8e3f-648474582%7CvcpLpG7BNiLF1QAyZgydkfQEBQU."));
+ FbDialog.this.dismiss();
+ }
+ }
+ }
+
+}
diff --git a/facebook/src/com/facebook/android/Util.java b/facebook/src/com/facebook/android/Util.java
new file mode 100644
index 0000000000..d3969df4ec
--- /dev/null
+++ b/facebook/src/com/facebook/android/Util.java
@@ -0,0 +1,107 @@
+package com.facebook.android;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * @author ssoneff@facebook.com
+ *
+ */
+public final class Util {
+
+ public static interface Callback {
+ public void call(String result);
+ }
+
+ private static final String STRING_BOUNDARY = "$$BOUNDARYajsfljas;l-#019823092";
+
+ public static String encodeUrl(Bundle b) {
+ if (b == null) return "";
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (String key : b.keySet()) {
+ if (first) first = false; else sb.append("&");
+ sb.append(key + "=" + b.getString(key));
+ }
+ Log.d("Facebook-Util", "encode: " + sb.toString());
+ return sb.toString();
+ }
+
+ public static Bundle decodeUrl(String s) {
+ Log.d("Facebook-Util", "decode: " + s);
+ Bundle b = new Bundle();
+ if (s == null) return b;
+ String array[] = s.split("&");
+ for (String p : array) {
+ String v[] = p.split("=");
+ b.putString(v[0], v[1]);
+ }
+ return b;
+ }
+
+ public static Bundle parseUrl(String url) {
+ url = url.replace("fbconnect", "http"); // HACK to prevent MalformedURLException
+ URL u = null;
+ try {
+ u = new URL(url);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ return new Bundle();
+ }
+ Bundle b = decodeUrl(u.getQuery());
+ b.putAll(decodeUrl(u.getRef()));
+ return b;
+ }
+
+ public static String openUrl(String url, String method, String parameters) {
+ Log.d("Facebook-Util", "Opening URL: " + url + " Query: " + parameters);
+ try {
+ if (method.equals("GET")) { url = url + "?" + parameters; }
+ HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+ conn.setRequestMethod(method);
+ if (method.equals("POST")) {
+ conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + STRING_BOUNDARY);
+ conn.getOutputStream().write(parameters.getBytes("UTF-8"));
+ }
+ return read(conn.getInputStream());
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "";
+ }
+ }
+
+ private static String read(InputStream in) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ BufferedReader r = new BufferedReader(new InputStreamReader(in));
+ for (String inputLine; (inputLine = r.readLine()) != null; ) sb.append(inputLine);
+ in.close();
+ return sb.toString();
+ }
+
+ public static String join(String[] strings, char delimiter) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (String s : strings) {
+ if (first) first = false; else sb.append(delimiter);
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+
+ public static void asyncOpenUrl(final String url, final String httpMethod, final String parameters, final Callback callback) {
+ new Thread() {
+ @Override public void run() {
+ callback.call(openUrl(url, httpMethod, parameters));
+ }
+ }.run();
+ }
+
+}