Skip to content

Commit

Permalink
First Draft
Browse files Browse the repository at this point in the history
  • Loading branch information
Steven Soneff authored and Steven Soneff committed May 10, 2010
1 parent b119d7c commit 301afe5
Show file tree
Hide file tree
Showing 8 changed files with 483 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
*~
*.DS_Store
*.class
/facebook/bin/
7 changes: 7 additions & 0 deletions facebook/.classpath
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="lib" path="/Users/ssoneff/Downloads/android-sdk-mac_86/platforms/android-7/android.jar"/>

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

should remove this and the line underneath

<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="output" path="bin"/>
</classpath>
17 changes: 17 additions & 0 deletions facebook/.project
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>FacebookSDK</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
12 changes: 12 additions & 0 deletions 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
241 changes: 241 additions & 0 deletions 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 {

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

Can you add javadocs to the methods (especially the public ones)?

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

Ya definitely, on my list ;)


public static final String SUCCESS_URI = "fbconnect://success";
public static final String PREFS_KEY = "facebook-session";

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

for the cookie on websites, we use "fbs_{app_id}", the idea being that only instances bound to the same app id should share data

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

I don't think this really matters here, since the stored session information will be associated with the Android application context and hence only accessible from the present application (I assume that an Android app only has one Application ID); good point though


private static final String OAUTH_ENDPOINT = "http://graph.dev.facebook.com/oauth/authorize";

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

might be a good idea to make these protected. that way you can change to internal urls in a subclass when testing.

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

Since these fields are static, I don't think that making them protected would allow us to override them in a subclass (references would be bound to the static class type member variables and not looked up at runtime) -- but I agree that it would be nice to mock them out for testing, so perhaps I should make them non-final instance fields with get/setters?

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

sounds like a good idea. add corresponding instance variables and use the static values as the defaults

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";

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

use access_token instead, thats what all the other sdk's use.

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

ok


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() {

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

deauth is different than logout. for logout, i think it should clear the pref store, and nothing else.

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

why not deauth and logout in the same operation? once you clear the pref store the token is no longer available so might as well deauth it

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

as per discussion with nshah, I agree that it would be better to just clear the local session information rather than run the de-auth API method

// TODO(ssoneff) how does logout work? de-auth api method??
// support multiple logout listeners?
}

public void authorize(String[] permissions, final DialogListener listener) {

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

we should remove authorize() and just have login() with an optional set of permissions. in most cases apps will want to tos the user and request permissions in one shot.

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

This function should return immediately if it has a non expired token AND all the requested permissions have been granted. This implies we should persist all the perms the user has granted the app. The reason is that if the developer updates the app to request for new permissions, the developer shouldn't also have to write custom logic to avoid the dialog. It should "just work."

Bundle params = new Bundle();
params.putString("display", "touch");

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

should this be a setting (default "touch" tho) -- i'm wondering that with android being used in various types of devices, it would be valuable to allow the developer to choose to use the regular flow just in case.

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

would be nice to handle this server-side: detect screen resolution, OS, etc. and just serve up the right format...

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, ','));

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

use { }

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

ok

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);

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

should format all code according to internal style guide -- 80cols

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

please use more descriptive (greater than 1 char) names (e.g. 'prefs').

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"));

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

where do we validate the incoming session?

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

what does validate the incoming session mean?

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) {

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

my java foo is weak -- what does the final qualifier mean here?

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

final makes the reference immutable: the listener is declared final so it can be referenced in the callback (this is a Java requirement to maintain consistency of the reference in the calling and callback contexts)

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() {

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

i think we should accept a null for parameters and default to new Bundle() in here if necessary. then the overloaded method above can also pass null as the third arg.

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

This comment has been minimized.

Copy link
@daaku

daaku May 10, 2010

remove

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

done

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() { }
}
}
13 changes: 13 additions & 0 deletions 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
}

}
82 changes: 82 additions & 0 deletions 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");

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

how about "Login with Facebook"? :)

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

I was planning to fetch the UI content and extract the correct title and size from this before displaying the UI dialog, thus displaying a dialog with the correct size and title automatically (and a loading dialog in the meantime). However, the webview crashes when I do this and it's not obvious how to fix it, so I'm using this title temporarily -- will try to fix soon.

}

@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)) {

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

indentation

This comment has been minimized.

Copy link
@soneff

soneff May 11, 2010

fixed

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."));

This comment has been minimized.

Copy link
@yariv

yariv May 11, 2010

indentation

FbDialog.this.dismiss();
}
}
}

}

0 comments on commit 301afe5

Please sign in to comment.