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