Merge commit '4417444235a32ed896050c312752c55246f695a5' as 'facebook'

pull/14/head
Tim Su 14 years ago
commit 13a7cf37a5

@ -0,0 +1,15 @@
*~
*.DS_Store
*.class
*java#
*.classpath
*.project
*.settings
facebook/bin/
facebook/gen/
examples/stream/bin/
examples/simple/bin/
examples/stream/gen/
examples/simple/gen/
tests/bin
tests/gen

Binary file not shown.

@ -0,0 +1,70 @@
This open source Java library allows you to integrate Facebook into your Android application. Except as otherwise noted, the Facebook Android SDK is licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)
Getting Started
===============
See our [Android SDK Getting Started Guide](http://developers.facebook.com/docs/guides/mobile#android)
Sample Applications
===============
This library includes two sample applications to guide you in development.
* __simple__: A bare-bones app that demonstrates authorization, making API calls, and invoking a dialog.
* __stream__: This slightly beefier application lets you view your news feed.
To install a sample application into Eclipse (3.5):
* Create the sample application in your workspace:
2. Select __File__ -> __New__ -> __Project__, choose __Android Project__, and then click __Next__.
3. Select "Create project from existing source".
4. Choose either __examples/simple__ or __examples/stream__. You should see the project properties populated.
5. Click Finish to continue.
* Build the project: from the Project menu, select "Build Project".
* Run the application: from the Run menu, select "Run Configurations...". Under Android Application, you can create a new run configuration: give it a name and select the simple Example project; use the default activity Launch Action. See http://developer.android.com/guide/developing/eclipse-adt.html#RunConfig for more details.
Testing
===============
Here are some tips to help test your application:
* You will need to have the Facebook application in your test environment. The SDK includes a developer release of the Facebook application that can be side-loaded for testing purposes. On an actual device, you can just download the latest version of the app from the Android Market, but on the emulator you will have to install it yourself:
adb install FBAndroid.apk
* Use a signed build. You can sign with a debug key, but make sure that the key you used to sign matches the __Key Hash__ field in the Facebook developer settings.
* Make sure to test both with and without the Facebook application. The SDK will fall back to a Webview if the Facebook app is not installed.
* You can use this [guide to developing on a device](http://developer.android.com/guide/developing/device.html).
Debugging
==========
Here's a few common errors and their solutions.
* __Build error: "missing gen files".__
This should go away when you rebuild the project. If you have trouble, try running __Clean...__ from the __Project__ menu.
* __Error: "invalid_key"__
This error means that the Facebook server doesn't recognize your Android key hash. Make sure that you correctly generated and copy/pasted your key hash into the Facebook developer settings console (http://www.facebook.com/developers/apps.php), and make sure that your application has been signed with the same key you registered with Facebook.
* __Dialog won't load or shows a blank screen.__
This can be tricky to debug. If the logs don't give an indication of what's wrong, I suggest installing tcpdump on your device and getting a trace. Tutorial: http://www.vbsteven.be/blog/android-debugging-inspectin-network-traffic-with-tcpdump/
If you still can't tell what's going on, then file an issue and please include the HTTP trace.
* __I can't upload photos with photos.upload.__
Make sure the Bundle value for the photo parameter is a byte array.
Report Issues/Bugs
===============
[http://bugs.developers.facebook.net/enter_bug.cgi?product=SDKs](http://bugs.developers.facebook.net/enter_bug.cgi?product=SDKs)

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.android"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon"
android:label="@string/app_name"
android:debuggable="true">
<activity android:name=".Example"
android:label="@string/app_name"
android:configChanges="keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-sdk android:minSdkVersion="3"/>
</manifest>

@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Indicates whether an apk should be generated for each density.
split.density=false
android.library.reference.1=../../facebook/
# Project target.
target=android-3

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/login_down" />
<item android:drawable="@drawable/login" /> <!-- default -->
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/logout_down" />
<item android:drawable="@drawable/logout" /> <!-- default -->
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:background="@drawable/white"
android:gravity="center_horizontal">
<com.facebook.android.LoginButton
android:id="@+id/login"
android:src="@drawable/login_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="30dp"
/>
<TextView android:id="@+id/txt"
android:text="@string/hello"
android:textColor="@drawable/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button android:id="@+id/uploadButton"
android:text="@string/upload"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="20dp"
android:paddingLeft="20dp"
android:layout_margin="20dp"
/>
<Button android:id="@+id/requestButton"
android:text="@string/request"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="20dp"
android:paddingLeft="20dp"
android:layout_margin="20dp"
/>
<Button android:id="@+id/postButton"
android:text="@string/post"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="20dp"
android:paddingLeft="20dp"
android:layout_margin="20dp"
/>
<Button android:id="@+id/deletePostButton"
android:text="@string/delete"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="20dp"
android:paddingLeft="20dp"
android:layout_margin="20dp"
/>
</LinearLayout>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<drawable name="white">#ffffff</drawable>
<drawable name="black">#000000</drawable>
</resources>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Facebook SDK Example</string>
<string name="request">Request!</string>
<string name="hello">Hello World!</string>
<string name="upload">Upload Photo</string><string name="post">Wall Post!</string>
<string name="delete">Delete Post</string>
</resources>

@ -0,0 +1,23 @@
package com.facebook.android;
import com.facebook.android.Facebook.DialogListener;
/**
* Skeleton base class for RequestListeners, providing default error
* handling. Applications should handle these error conditions.
*
*/
public abstract class BaseDialogListener implements DialogListener {
public void onFacebookError(FacebookError e) {
e.printStackTrace();
}
public void onError(DialogError e) {
e.printStackTrace();
}
public void onCancel() {
}
}

@ -0,0 +1,40 @@
package com.facebook.android;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import android.util.Log;
import com.facebook.android.AsyncFacebookRunner.RequestListener;
/**
* Skeleton base class for RequestListeners, providing default error
* handling. Applications should handle these error conditions.
*
*/
public abstract class BaseRequestListener implements RequestListener {
public void onFacebookError(FacebookError e, final Object state) {
Log.e("Facebook", e.getMessage());
e.printStackTrace();
}
public void onFileNotFoundException(FileNotFoundException e,
final Object state) {
Log.e("Facebook", e.getMessage());
e.printStackTrace();
}
public void onIOException(IOException e, final Object state) {
Log.e("Facebook", e.getMessage());
e.printStackTrace();
}
public void onMalformedURLException(MalformedURLException e,
final Object state) {
Log.e("Facebook", e.getMessage());
e.printStackTrace();
}
}

@ -0,0 +1,281 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import com.facebook.android.SessionEvents.AuthListener;
import com.facebook.android.SessionEvents.LogoutListener;
public class Example extends Activity {
// Your Facebook Application ID must be set before running this example
// See http://www.facebook.com/developers/createapp.php
public static final String APP_ID = "175729095772478";
private LoginButton mLoginButton;
private TextView mText;
private Button mRequestButton;
private Button mPostButton;
private Button mDeleteButton;
private Button mUploadButton;
private Facebook mFacebook;
private AsyncFacebookRunner mAsyncRunner;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (APP_ID == null) {
Util.showAlert(this, "Warning", "Facebook Applicaton ID must be " +
"specified before running this example: see Example.java");
}
setContentView(R.layout.main);
mLoginButton = (LoginButton) findViewById(R.id.login);
mText = (TextView) Example.this.findViewById(R.id.txt);
mRequestButton = (Button) findViewById(R.id.requestButton);
mPostButton = (Button) findViewById(R.id.postButton);
mDeleteButton = (Button) findViewById(R.id.deletePostButton);
mUploadButton = (Button) findViewById(R.id.uploadButton);
mFacebook = new Facebook(APP_ID);
mAsyncRunner = new AsyncFacebookRunner(mFacebook);
SessionStore.restore(mFacebook, this);
SessionEvents.addAuthListener(new SampleAuthListener());
SessionEvents.addLogoutListener(new SampleLogoutListener());
mLoginButton.init(this, mFacebook);
mRequestButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mAsyncRunner.request("me", new SampleRequestListener());
}
});
mRequestButton.setVisibility(mFacebook.isSessionValid() ?
View.VISIBLE :
View.INVISIBLE);
mUploadButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
Bundle params = new Bundle();
params.putString("method", "photos.upload");
URL uploadFileUrl = null;
try {
uploadFileUrl = new URL(
"http://www.facebook.com/images/devsite/iphone_connect_btn.jpg");
} catch (MalformedURLException e) {
e.printStackTrace();
}
try {
HttpURLConnection conn= (HttpURLConnection)uploadFileUrl.openConnection();
conn.setDoInput(true);
conn.connect();
int length = conn.getContentLength();
byte[] imgData =new byte[length];
InputStream is = conn.getInputStream();
is.read(imgData);
params.putByteArray("picture", imgData);
} catch (IOException e) {
e.printStackTrace();
}
mAsyncRunner.request(null, params, "POST",
new SampleUploadListener(), null);
}
});
mUploadButton.setVisibility(mFacebook.isSessionValid() ?
View.VISIBLE :
View.INVISIBLE);
mPostButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mFacebook.dialog(Example.this, "feed",
new SampleDialogListener());
}
});
mPostButton.setVisibility(mFacebook.isSessionValid() ?
View.VISIBLE :
View.INVISIBLE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
mFacebook.authorizeCallback(requestCode, resultCode, data);
}
public class SampleAuthListener implements AuthListener {
public void onAuthSucceed() {
mText.setText("You have logged in! ");
mRequestButton.setVisibility(View.VISIBLE);
mUploadButton.setVisibility(View.VISIBLE);
mPostButton.setVisibility(View.VISIBLE);
}
public void onAuthFail(String error) {
mText.setText("Login Failed: " + error);
}
}
public class SampleLogoutListener implements LogoutListener {
public void onLogoutBegin() {
mText.setText("Logging out...");
}
public void onLogoutFinish() {
mText.setText("You have logged out! ");
mRequestButton.setVisibility(View.INVISIBLE);
mUploadButton.setVisibility(View.INVISIBLE);
mPostButton.setVisibility(View.INVISIBLE);
}
}
public class SampleRequestListener extends BaseRequestListener {
public void onComplete(final String response, final Object state) {
try {
// process the response here: executed in background thread
Log.d("Facebook-Example", "Response: " + response.toString());
JSONObject json = Util.parseJson(response);
final String name = json.getString("name");
// then post the processed result back to the UI thread
// if we do not do this, an runtime exception will be generated
// e.g. "CalledFromWrongThreadException: Only the original
// thread that created a view hierarchy can touch its views."
Example.this.runOnUiThread(new Runnable() {
public void run() {
mText.setText("Hello there, " + name + "!");
}
});
} catch (JSONException e) {
Log.w("Facebook-Example", "JSON Error in response");
} catch (FacebookError e) {
Log.w("Facebook-Example", "Facebook Error: " + e.getMessage());
}
}
}
public class SampleUploadListener extends BaseRequestListener {
public void onComplete(final String response, final Object state) {
try {
// process the response here: (executed in background thread)
Log.d("Facebook-Example", "Response: " + response.toString());
JSONObject json = Util.parseJson(response);
final String src = json.getString("src");
// then post the processed result back to the UI thread
// if we do not do this, an runtime exception will be generated
// e.g. "CalledFromWrongThreadException: Only the original
// thread that created a view hierarchy can touch its views."
Example.this.runOnUiThread(new Runnable() {
public void run() {
mText.setText("Hello there, photo has been uploaded at \n" + src);
}
});
} catch (JSONException e) {
Log.w("Facebook-Example", "JSON Error in response");
} catch (FacebookError e) {
Log.w("Facebook-Example", "Facebook Error: " + e.getMessage());
}
}
}
public class WallPostRequestListener extends BaseRequestListener {
public void onComplete(final String response, final Object state) {
Log.d("Facebook-Example", "Got response: " + response);
String message = "<empty>";
try {
JSONObject json = Util.parseJson(response);
message = json.getString("message");
} catch (JSONException e) {
Log.w("Facebook-Example", "JSON Error in response");
} catch (FacebookError e) {
Log.w("Facebook-Example", "Facebook Error: " + e.getMessage());
}
final String text = "Your Wall Post: " + message;
Example.this.runOnUiThread(new Runnable() {
public void run() {
mText.setText(text);
}
});
}
}
public class WallPostDeleteListener extends BaseRequestListener {
public void onComplete(final String response, final Object state) {
if (response.equals("true")) {
Log.d("Facebook-Example", "Successfully deleted wall post");
Example.this.runOnUiThread(new Runnable() {
public void run() {
mDeleteButton.setVisibility(View.INVISIBLE);
mText.setText("Deleted Wall Post");
}
});
} else {
Log.d("Facebook-Example", "Could not delete wall post");
}
}
}
public class SampleDialogListener extends BaseDialogListener {
public void onComplete(Bundle values) {
final String postId = values.getString("post_id");
if (postId != null) {
Log.d("Facebook-Example", "Dialog Success! post_id=" + postId);
mAsyncRunner.request(postId, new WallPostRequestListener());
mDeleteButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mAsyncRunner.request(postId, new Bundle(), "DELETE",
new WallPostDeleteListener(), null);
}
});
mDeleteButton.setVisibility(View.VISIBLE);
} else {
Log.d("Facebook-Example", "No wall post made");
}
}
}
}

@ -0,0 +1,139 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
import com.facebook.android.BaseRequestListener;
import com.facebook.android.SessionEvents.AuthListener;
import com.facebook.android.SessionEvents.LogoutListener;
import com.facebook.android.Facebook.DialogListener;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageButton;
public class LoginButton extends ImageButton {
private Facebook mFb;
private Handler mHandler;
private SessionListener mSessionListener = new SessionListener();
private String[] mPermissions;
private Activity mActivity;
public LoginButton(Context context) {
super(context);
}
public LoginButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LoginButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void init(final Activity activity, final Facebook fb) {
init(activity, fb, new String[] {});
}
public void init(final Activity activity, final Facebook fb,
final String[] permissions) {
mActivity = activity;
mFb = fb;
mPermissions = permissions;
mHandler = new Handler();
setBackgroundColor(Color.TRANSPARENT);
setAdjustViewBounds(true);
setImageResource(fb.isSessionValid() ?
R.drawable.logout_button :
R.drawable.login_button);
drawableStateChanged();
SessionEvents.addAuthListener(mSessionListener);
SessionEvents.addLogoutListener(mSessionListener);
setOnClickListener(new ButtonOnClickListener());
}
private final class ButtonOnClickListener implements OnClickListener {
public void onClick(View arg0) {
if (mFb.isSessionValid()) {
SessionEvents.onLogoutBegin();
AsyncFacebookRunner asyncRunner = new AsyncFacebookRunner(mFb);
asyncRunner.logout(getContext(), new LogoutRequestListener());
} else {
mFb.authorize(mActivity, mPermissions,
new LoginDialogListener());
}
}
}
private final class LoginDialogListener implements DialogListener {
public void onComplete(Bundle values) {
SessionEvents.onLoginSuccess();
}
public void onFacebookError(FacebookError error) {
SessionEvents.onLoginError(error.getMessage());
}
public void onError(DialogError error) {
SessionEvents.onLoginError(error.getMessage());
}
public void onCancel() {
SessionEvents.onLoginError("Action Canceled");
}
}
private class LogoutRequestListener extends BaseRequestListener {
public void onComplete(String response, final Object state) {
// callback should be run in the original thread,
// not the background thread
mHandler.post(new Runnable() {
public void run() {
SessionEvents.onLogoutFinish();
}
});
}
}
private class SessionListener implements AuthListener, LogoutListener {
public void onAuthSucceed() {
setImageResource(R.drawable.logout_button);
SessionStore.save(mFb, getContext());
}
public void onAuthFail(String error) {
}
public void onLogoutBegin() {
}
public void onLogoutFinish() {
SessionStore.clear(getContext());
setImageResource(R.drawable.login_button);
}
}
}

@ -0,0 +1,146 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
import java.util.LinkedList;
public class SessionEvents {
private static LinkedList<AuthListener> mAuthListeners =
new LinkedList<AuthListener>();
private static LinkedList<LogoutListener> mLogoutListeners =
new LinkedList<LogoutListener>();
/**
* Associate the given listener with this Facebook object. The listener's
* callback interface will be invoked when authentication events occur.
*
* @param listener
* The callback object for notifying the application when auth
* events happen.
*/
public static void addAuthListener(AuthListener listener) {
mAuthListeners.add(listener);
}
/**
* Remove the given listener from the list of those that will be notified
* when authentication events occur.
*
* @param listener
* The callback object for notifying the application when auth
* events happen.
*/
public static void removeAuthListener(AuthListener listener) {
mAuthListeners.remove(listener);
}
/**
* Associate the given listener with this Facebook object. The listener's
* callback interface will be invoked when logout occurs.
*
* @param listener
* The callback object for notifying the application when log out
* starts and finishes.
*/
public static void addLogoutListener(LogoutListener listener) {
mLogoutListeners.add(listener);
}
/**
* Remove the given listener from the list of those that will be notified
* when logout occurs.
*
* @param listener
* The callback object for notifying the application when log out
* starts and finishes.
*/
public static void removeLogoutListener(LogoutListener listener) {
mLogoutListeners.remove(listener);
}
public static void onLoginSuccess() {
for (AuthListener listener : mAuthListeners) {
listener.onAuthSucceed();
}
}
public static void onLoginError(String error) {
for (AuthListener listener : mAuthListeners) {
listener.onAuthFail(error);
}
}
public static void onLogoutBegin() {
for (LogoutListener l : mLogoutListeners) {
l.onLogoutBegin();
}
}
public static void onLogoutFinish() {
for (LogoutListener l : mLogoutListeners) {
l.onLogoutFinish();
}
}
/**
* Callback interface for authorization events.
*
*/
public static interface AuthListener {
/**
* Called when a auth flow completes successfully and a valid OAuth
* Token was received.
*
* Executed by the thread that initiated the authentication.
*
* API requests can now be made.
*/
public void onAuthSucceed();
/**
* Called when a login completes unsuccessfully with an error.
*
* Executed by the thread that initiated the authentication.
*/
public void onAuthFail(String error);
}
/**
* Callback interface for logout events.
*
*/
public static interface LogoutListener {
/**
* Called when logout begins, before session is invalidated.
* Last chance to make an API call.
*
* Executed by the thread that initiated the logout.
*/
public void onLogoutBegin();
/**
* Called when the session information has been cleared.
* UI should be updated to reflect logged-out state.
*
* Executed by the thread that initiated the logout.
*/
public void onLogoutFinish();
}
}

@ -0,0 +1,53 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
import com.facebook.android.Facebook;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
public class SessionStore {
private static final String TOKEN = "access_token";
private static final String EXPIRES = "expires_in";
private static final String KEY = "facebook-session";
public static boolean save(Facebook session, Context context) {
Editor editor =
context.getSharedPreferences(KEY, Context.MODE_PRIVATE).edit();
editor.putString(TOKEN, session.getAccessToken());
editor.putLong(EXPIRES, session.getAccessExpires());
return editor.commit();
}
public static boolean restore(Facebook session, Context context) {
SharedPreferences savedSession =
context.getSharedPreferences(KEY, Context.MODE_PRIVATE);
session.setAccessToken(savedSession.getString(TOKEN, null));
session.setAccessExpires(savedSession.getLong(EXPIRES, 0));
return session.isSessionValid();
}
public static void clear(Context context) {
Editor editor =
context.getSharedPreferences(KEY, Context.MODE_PRIVATE).edit();
editor.clear();
editor.commit();
}
}

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0" package="com.facebook.stream">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".App"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-sdk android:minSdkVersion="3" />
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,14 @@
#main {
width: 200px;
margin: 50% auto 0 auto;
text-align: center;
}
#welcome {
margin-bottom: 10px;
}
#login_button {
width: 91px;
margin: auto;
}

@ -0,0 +1,13 @@
<html>
<head>
<link rel="stylesheet" href="file:///android_asset/login.css" type="text/css"/>
</head>
<body>
<div id="main">
<div id="welcome">Welcome to Stream</div>
<div id="login_button">
<a onclick="app.login()"><img src="file:///android_asset/login_button.png" width="90" height="31"/></a>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -0,0 +1,118 @@
a {
color: #00f;
}
a:visited {
color: #00f;
}
.hidden {
display: none;
}
.clear {
clear: both
}
#header {
float: right;
}
input {
width: 80%;
height: 28px;
border: #666 solid 1px;
margin: 1px 3px 0 0;
float: left;
}
#status_box {
margin: 5px 0;
}
#status_input {
color: #666;
float: left;
}
.profile_pic_container {
float: left;
margin: 0 5px;
}
.profile_pic {
width: 30px;
height: 30px;
}
.attachment {
margin-top: 3px;
}
.title {
margin-bottom: 3px;
}
.caption {
margin-bottom: 3px;
color: #666;
}
.description {
color: #666;
}
.picture {
float: left;
margin-right: 5px;
}
.post {
padding: 10px 0;
border-bottom: #ccc solid 1px;
}
.action_links {
margin: 5px 0;
}
.action_link {
margin-right: 5px;
float: left;
}
.comment {
padding: 5px 0;
margin-bottom: 2px;
background-color: #eee;
min-height: 30px;
}
.comments {
margin-top: 5px;
}
.comment_box {
display: none;
}
.timestamp {
color: #666;
}
.like_icon {
float: left;
top: 3px;
position: relative;
margin-right: 5px;
}
.like_icon img {
width: 16px;
height: 14px;
}
.num_likes {
padding-left: 5px;
}

@ -0,0 +1,88 @@
function $(id) {
return document.getElementById(id);
}
function show(id) {
$(id).style.display = "block";
}
function hide(id) {
$(id).style.display = "none";
}
function onStatusBoxFocus(elt) {
elt.value = '';
elt.style.color = "#000";
show('status_submit');
}
function updateStatus() {
var message = $('status_input').value;
if (message == "") {
return;
}
$('status_input').disabled = true;
$('status_submit').disabled = true;
app.updateStatus(message);
}
function onStatusUpdated(html) {
$('status_input').disabled = false;
$('status_submit').disabled = false;
$('posts').innerHTML = html + $('posts').innerHTML;
}
function like(post_id) {
doLike(post_id, true);
}
function unlike(post_id) {
doLike(post_id, false);
}
function doLike(post_id, val) {
var ids = getLikeLinkIds(post_id, val);
$(ids[0]).disabled = true;
app.like(post_id, val);
}
// called when the api request has succeeded
function onLike(post_id, val) {
var ids = getLikeLinkIds(post_id, val);
$(ids[0]).disabled = false;
hide(ids[0]);
show(ids[1]);
}
function getLikeLinkIds(post_id, val) {
if (val) {
var prefix1 = 'like';
var prefix2 = 'unlike';
} else {
var prefix1 = 'unlike';
var prefix2 = 'like';
}
return [prefix1 + post_id, prefix2 + post_id];
}
function comment(post_id) {
show("comment_box" + post_id);
$("comment_box_input" + post_id).focus();
}
function postComment(post_id) {
var message = $("comment_box_input" + post_id).value;
if (message == "") {
return;
}
$("comment_box" + post_id).disabled = true;
app.postComment(post_id, message);
}
function onComment(post_id, html) {
$("comments" + post_id).innerHTML += html;
$("comment_box" + post_id).disabled = false;
$("comment_box_input" + post_id).value = "";
}

@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Indicates whether an apk should be generated for each density.
split.density=false
android.library.reference.1=../../facebook/
# Project target.
target=android-3

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">stream</string>
</resources>

@ -0,0 +1,70 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import android.app.Activity;
import android.app.AlertDialog.Builder;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.android.Facebook;
/**
* This class implements the application's main Activity.
*
* @author yariv
*/
public class App extends Activity {
// This is a demo application ID just to get this demo up and running
// If you modify this to work for your own app, you must use your
// own Facebook Application ID.
// See http://www.facebook.com/developers/createapp.php
public static final String FB_APP_ID = "126642314059639";
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (FB_APP_ID == null) {
Builder alertBuilder = new Builder(this);
alertBuilder.setTitle("Warning");
alertBuilder.setMessage("A Facebook Applicaton ID must be " +
"specified before running this example: see App.java");
alertBuilder.create().show();
}
// Initialize the dispatcher
Dispatcher dispatcher = new Dispatcher(this);
dispatcher.addHandler("login", LoginHandler.class);
dispatcher.addHandler("stream", StreamHandler.class);
dispatcher.addHandler("logout", LogoutHandler.class);
// If a session already exists, render the stream page
// immediately. Otherwise, render the login page.
Session session = Session.restore(this);
if (session != null) {
dispatcher.runHandler("stream");
} else {
dispatcher.runHandler("login");
}
}
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
Facebook fb = Session.wakeupForAuthCallback();
fb.authorizeCallback(requestCode, resultCode, data);
}
}

@ -0,0 +1,67 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;
import com.facebook.android.FacebookError;
import com.facebook.android.Util;
import com.facebook.android.AsyncFacebookRunner.RequestListener;
abstract class AsyncRequestListener implements RequestListener {
public void onComplete(String response, final Object state) {
try {
JSONObject obj = Util.parseJson(response);
onComplete(obj, state);
} catch (JSONException e) {
e.printStackTrace();
Log.e("facebook-stream", "JSON Error:" + e.getMessage());
} catch (FacebookError e) {
Log.e("facebook-stream", "Facebook Error:" + e.getMessage());
}
}
public abstract void onComplete(JSONObject obj, final Object state);
public void onFacebookError(FacebookError e, final Object state) {
Log.e("stream", "Facebook Error:" + e.getMessage());
}
public void onFileNotFoundException(FileNotFoundException e,
final Object state) {
Log.e("stream", "Resource not found:" + e.getMessage());
}
public void onIOException(IOException e, final Object state) {
Log.e("stream", "Network Error:" + e.getMessage());
}
public void onMalformedURLException(MalformedURLException e,
final Object state) {
Log.e("stream", "Invalid URL:" + e.getMessage());
}
}

@ -0,0 +1,192 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import java.util.HashMap;
import android.app.Activity;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
/**
* Handles the rendering of the WebView instance and the
* mapping of app:// urls to their appropriate Handlers.
*
* @author yariv
*/
public class Dispatcher {
// The WebView instance
private WebView webView;
// The app's main Activity
private Activity activity;
// Contains the webView object
LinearLayout layout;
// Has the webView been rendered?
boolean isWebViewShown;
// Holds mappings between handler names to their classes
// (e.g. "login" -> LoginHandler.class)
HashMap<String, Class> handlers;
public Dispatcher(Activity activity) {
this.activity = activity;
handlers = new HashMap<String, Class>();
layout = new LinearLayout(activity);
activity.addContentView(
layout, new LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
isWebViewShown = false;
showWebView();
}
/**
* Adds a handler name to handler class mapping. This should be called
* for each handler when the application starts up.
*
* @param name
* @param clazz
*/
public void addHandler(String name, Class clazz) {
this.handlers.put(name, clazz);
}
/**
* Executes the handler associated with the given name. For example,
* dispatcher.runHandler("login") would render the Login page in the
* WebView instance.
*
* @param name
*/
public void runHandler(String name) {
Class clazz = handlers.get(name);
if (clazz != null) {
try {
Handler handler = (Handler)clazz.newInstance();
handler.setDispatcher(this);
handler.go();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
/**
* Show the app's WebView instance.
*/
public void showWebView() {
if (isWebViewShown) {
return;
}
webView = new WebView(activity);
webView.setWebViewClient(new AppWebViewClient());
webView.getSettings().setJavaScriptEnabled(true);
layout.addView(webView,
new LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
isWebViewShown = true;
}
/**
* Hide the app's WebView instance. This should be called if the
* WebView instance is visible and the app wants to open another
* WebView instance (e.g. for a Facebook dialog). Android doesn't
* seem to be able to handle more than one WebView instance per
* application.
*/
public void hideWebView() {
layout.removeView(webView);
isWebViewShown = false;
}
/**
* Returns true if the WebView instance is visible.
*/
public boolean isWebViewShown() {
return isWebViewShown;
}
/**
* Loads the html string into the WebView instance.
*
* @param html
*/
public void loadData(String html) {
webView.loadDataWithBaseURL(
"http://nada", html, "text/html", "utf8", "");
}
/**
* Loads a file from the assets directory into the
* WebView instance.
*
* @param file
*/
public void loadFile(String file) {
webView.loadUrl(getAbsoluteUrl(file));
}
/**
* Returns the absolute URL for a local file.
*
* @param file
*/
public static String getAbsoluteUrl(String file) {
return "file:///android_asset/" + file;
}
/**
* Returns the Dispatcher's WebView instance.
*/
public WebView getWebView() {
return webView;
}
/**
* Returns the Dispatcher's Activity
*/
public Activity getActivity() {
return activity;
}
/**
* Enables the mapping of app:// urls to Handlers.
*
* @author yariv
*/
private class AppWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("app://")) {
String handlerName = url.substring(6);
runHandler(handlerName);
return true;
}
return false;
}
}
}

@ -0,0 +1,76 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import android.app.Activity;
/**
* Helpers for doing basic file IO.
*
* @author yariv
*
*/
public class FileIO {
/**
* Write the data to the file indicate by fileName. The file is created
* if it doesn't exist.
*
* @param activity
* @param data
* @param fileName
* @throws IOException
*/
public static void write(
Activity activity, String data, String fileName)
throws IOException {
FileOutputStream fo = activity.openFileOutput(fileName, 0);
BufferedWriter bf = new BufferedWriter(new FileWriter(fo.getFD()));
bf.write(data);
bf.flush();
bf.close();
}
/**
* Read the contents of the file indicated by fileName
*
* @param activity
* @param fileName
* @return the contents
* @throws IOException
*/
public static String read(Activity activity, String fileName)
throws IOException {
FileInputStream is = activity.openFileInput(fileName);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
while (br.ready()) {
String line = br.readLine();
sb.append(line);
}
String data = sb.toString();
return data;
}
}

@ -0,0 +1,70 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import android.app.Activity;
import android.webkit.WebView;
/**
* An abstract superclass for handlers. Handlers are similar to
* controllers in traditional web apps. Each page has a handler
* that is responsible for rendering the page.
*
* @author yariv
*/
public abstract class Handler {
// The app's dispatcher.
protected Dispatcher dispatcher;
/**
* The dispatcher calls this method when the Handler
* is expected to render its page.
*/
public abstract void go();
/**
* A setter for the dispatcher.
*
* @param dispatcher
*/
public void setDispatcher(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
/**
* Returns the dispatcher.
*/
public Dispatcher getDispatcher() {
return dispatcher;
}
/**
* Returns the dispatcher's WebView
*/
public WebView getWebView() {
return dispatcher.getWebView();
}
/**
* Returns the dispatcher's Activity
*/
public Activity getActivity() {
return dispatcher.getActivity();
}
}

@ -0,0 +1,126 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import org.json.JSONObject;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import com.facebook.android.AsyncFacebookRunner;
import com.facebook.android.DialogError;
import com.facebook.android.Facebook;
import com.facebook.android.FacebookError;
import com.facebook.android.Facebook.DialogListener;
/**
* A handler for the login page.
*
* @author yariv
*/
public class LoginHandler extends Handler {
// The permissions that the app should request from the user
// when the user authorizes the app.
private static String[] PERMISSIONS =
new String[] { "offline_access", "read_stream", "publish_stream" };
/**
* Render the Login page.
*/
public void go() {
dispatcher.getWebView().addJavascriptInterface(
new JsHandler(), "app");
dispatcher.loadFile("login.html");
}
/**
* Contains functions that are exported to the Javascript context
* in Login.html
*
* @author yariv
*/
private class JsHandler {
/**
* Opens the Facebook login dialog.
*/
public void login() {
final Activity activity = LoginHandler.this.getActivity();
activity.runOnUiThread(new Runnable() {
public void run() {
// We need to temporarily remove the app's WebView
// instance because Android apparently doesn't support
// multiple WebView instances in the same app.
dispatcher.hideWebView();
final Facebook fb = new Facebook(App.FB_APP_ID);
Session.waitForAuthCallback(fb);
fb.authorize(getActivity(), PERMISSIONS,
new AppLoginListener(fb));
}
});
}
private class AppLoginListener implements DialogListener {
private Facebook fb;
public AppLoginListener(Facebook fb) {
this.fb = fb;
}
public void onCancel() {
Log.d("app", "login canceled");
}
public void onComplete(Bundle values) {
/**
* We request the user's info so we can cache it locally and
* use it to render the new html snippets
* when the user updates her status or comments on a post.
*/
new AsyncFacebookRunner(fb).request("/me",
new AsyncRequestListener() {
public void onComplete(JSONObject obj, final Object state) {
// save the session data
String uid = obj.optString("id");
String name = obj.optString("name");
new Session(fb, uid, name).save(getActivity());
// render the Stream page in the UI thread
getActivity().runOnUiThread(new Runnable() {
public void run() {
dispatcher.showWebView();
dispatcher.runHandler("stream");
}
});
}
}, null);
}
public void onError(DialogError e) {
Log.d("app", "dialog error: " + e);
}
public void onFacebookError(FacebookError e) {
Log.d("app", "facebook error: " + e);
}
}
}
}

@ -0,0 +1,78 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import android.util.Log;
import com.facebook.android.AsyncFacebookRunner;
import com.facebook.android.Facebook;
import com.facebook.android.FacebookError;
import com.facebook.android.AsyncFacebookRunner.RequestListener;
/**
* A handler for the logout link. This handler doesn't render
* its own page. After logging out it redirects the user
* to the login handler.
*
* @author yariv
*/
public class LogoutHandler extends Handler {
/**
* Called by the dispatcher when the user clicks 'logout'.
*/
public void go() {
Facebook fb = Session.restore(getActivity()).getFb();
// clear the local session data
Session.clearSavedSession(getActivity());
new AsyncFacebookRunner(fb).logout(getActivity(),
new RequestListener() {
public void onComplete(String response, final Object state) {
dispatcher.runHandler("login");
}
public void onFileNotFoundException(FileNotFoundException error,
final Object state) {
Log.e("app", error.toString());
dispatcher.runHandler("login");
}
public void onIOException(IOException error, final Object state) {
Log.e("app", error.toString());
dispatcher.runHandler("login");
}
public void onMalformedURLException(MalformedURLException error,
final Object state) {
Log.e("app", error.toString());
dispatcher.runHandler("login");
}
public void onFacebookError(FacebookError error,
final Object state) {
Log.e("app", error.toString());
dispatcher.runHandler("login");
}
});
}
}

@ -0,0 +1,174 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import com.facebook.android.Facebook;
/**
* A utility class for storing and retrieving Facebook session data.
*
* @author yariv
*/
public class Session {
private static final String TOKEN = "access_token";
private static final String EXPIRES = "expires_in";
private static final String KEY = "facebook-session";
private static final String UID = "uid";
private static final String NAME = "name";
private static final String APP_ID = "app_id";
private static Session singleton;
private static Facebook fbLoggingIn;
// The Facebook object
private Facebook fb;
// The user id of the logged in user
private String uid;
// The user name of the logged in user
private String name;
/**
* Constructor
*
* @param fb
* @param uid
* @param name
*/
public Session(Facebook fb, String uid, String name) {
this.fb = fb;
this.uid = uid;
this.name = name;
}
/**
* Returns the Facebook object
*/
public Facebook getFb() {
return fb;
}
/**
* Returns the session user's id
*/
public String getUid() {
return uid;
}
/**
* Returns the session user's name
*/
public String getName() {
return name;
}
/**
* Stores the session data on disk.
*
* @param context
* @return
*/
public boolean save(Context context) {
Editor editor =
context.getSharedPreferences(KEY, Context.MODE_PRIVATE).edit();
editor.putString(TOKEN, fb.getAccessToken());
editor.putLong(EXPIRES, fb.getAccessExpires());
editor.putString(UID, uid);
editor.putString(NAME, name);
editor.putString(APP_ID, fb.getAppId());
if (editor.commit()) {
singleton = this;
return true;
}
return false;
}
/**
* Loads the session data from disk.
*
* @param context
* @return
*/
public static Session restore(Context context) {
if (singleton != null) {
if (singleton.getFb().isSessionValid()) {
return singleton;
} else {
return null;
}
}
SharedPreferences prefs =
context.getSharedPreferences(KEY, Context.MODE_PRIVATE);
String appId = prefs.getString(APP_ID, null);
if (appId == null) {
return null;
}
Facebook fb = new Facebook(appId);
fb.setAccessToken(prefs.getString(TOKEN, null));
fb.setAccessExpires(prefs.getLong(EXPIRES, 0));
String uid = prefs.getString(UID, null);
String name = prefs.getString(NAME, null);
if (!fb.isSessionValid() || uid == null || name == null) {
return null;
}
Session session = new Session(fb, uid, name);
singleton = session;
return session;
}
/**
* Clears the saved session data.
*
* @param context
*/
public static void clearSavedSession(Context context) {
Editor editor =
context.getSharedPreferences(KEY, Context.MODE_PRIVATE).edit();
editor.clear();
editor.commit();
singleton = null;
}
/**
* Freezes a Facebook object while it's waiting for an auth callback.
*/
public static void waitForAuthCallback(Facebook fb) {
fbLoggingIn = fb;
}
/**
* Returns a Facebook object that's been waiting for an auth callback.
*/
public static Facebook wakeupForAuthCallback() {
Facebook fb = fbLoggingIn;
fbLoggingIn = null;
return fb;
}
}

@ -0,0 +1,118 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;
import com.facebook.android.AsyncFacebookRunner;
import com.facebook.android.Facebook;
import com.facebook.android.FacebookError;
import com.facebook.android.Util;
import com.facebook.android.AsyncFacebookRunner.RequestListener;
/**
* A handler for the stream page. It's responsible for
* fetching the stream data from the API and storing it
* in a local file based cache. It uses the helper class
* StreamRenderer to render the stream.
*
* @author yariv
*/
public class StreamHandler extends Handler {
private static final String CACHE_FILE = "cache.txt";
/**
* Called by the dispatcher to render the stream page.
*/
public void go() {
dispatcher.getWebView().addJavascriptInterface(
new StreamJsHandler(this), "app");
// first try to load the cached data
try {
String cached = FileIO.read(getActivity(), CACHE_FILE);
if (cached != null) {
JSONObject obj = new JSONObject(cached);
dispatcher.loadData(StreamRenderer.render(obj));
}
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
Facebook fb = Session.restore(getActivity()).getFb();
new AsyncFacebookRunner(fb).request("me/home",
new StreamRequestListener());
}
public class StreamRequestListener implements RequestListener {
public void onComplete(String response, final Object state) {
try {
JSONObject obj = Util.parseJson(response);
// try to cache the result
try {
FileIO.write(getActivity(), response, CACHE_FILE);
} catch (IOException e) {
e.printStackTrace();
}
// Convert the result into an HTML string and then load it
// into the WebView in the UI thread.
final String html = StreamRenderer.render(obj);
getActivity().runOnUiThread(new Runnable() {
public void run() {
dispatcher.loadData(html);
}
});
} catch (JSONException e) {
Log.e("stream", "JSON Error:" + e.getMessage());
} catch (FacebookError e) {
Log.e("stream", "Facebook Error:" + e.getMessage());
}
}
public void onFacebookError(FacebookError e, final Object state) {
Log.e("stream", "Facebook Error:" + e.getMessage());
}
public void onFileNotFoundException(FileNotFoundException e,
final Object state) {
Log.e("stream", "Resource not found:" + e.getMessage());
}
public void onIOException(IOException e, final Object state) {
Log.e("stream", "Network Error:" + e.getMessage());
}
public void onMalformedURLException(MalformedURLException e,
final Object state) {
Log.e("stream", "Invalid URL:" + e.getMessage());
}
}
}

@ -0,0 +1,205 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.os.Bundle;
import com.facebook.android.AsyncFacebookRunner;
import com.facebook.android.Facebook;
/**
* Implements functions that can be called from Javascript in the
* stream page.
*
* @author yariv
*/
class StreamJsHandler {
// The handler for the Stream page
private final StreamHandler streamHandler;
/**
* @param streamHandler
*/
StreamJsHandler(StreamHandler streamHandler) {
this.streamHandler = streamHandler;
}
/**
* Returns the Facebook object.
*/
private AsyncFacebookRunner getFb() {
Facebook fb = Session.restore(streamHandler.getActivity()).getFb();
return new AsyncFacebookRunner(fb);
}
/**
* Update the status and render the resulting status at the
* top of the stream.
*
* @param message
*/
public void updateStatus(final String message) {
AsyncFacebookRunner fb = getFb();
Bundle params = new Bundle();
params.putString("message", message);
fb.request("me/feed", params, "POST", new AsyncRequestListener() {
public void onComplete(JSONObject obj, final Object state) {
String html;
try {
html = renderStatus(obj, message);
html = html.replace("'", "\\\'");
callJs("onStatusUpdated('" + html + "');");
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, null);
}
/**
* Renders the html for the new status.
*
* @param response
* @param message
* @return
* @throws JSONException
*/
private String renderStatus(JSONObject response, String message)
throws JSONException {
String postId = response.getString("id");
JSONObject post = new JSONObject();
post.put("id", postId);
post.put("message", message);
JSONObject from = createAuthorObj();
post.put("from", from);
JSONArray actions = new JSONArray();
JSONObject like = new JSONObject();
like.put("name", "Like");
actions.put(like);
JSONObject comment = new JSONObject();
comment.put("name", "Comment");
actions.put(comment);
post.put("actions", actions);
SimpleDateFormat format = StreamRenderer.getDateFormat();
String timestamp = format.format(new Date());
post.put("created_time", timestamp);
String html = StreamRenderer.renderSinglePost(post);
return html;
}
/**
* Like or unlike a post
*
* @param post_id
* @param val if the action should be a like (true) or an unlike (false)
*/
public void like(final String post_id, final boolean val) {
Bundle params = new Bundle();
if (!val) {
params.putString("method", "delete");
}
getFb().request(post_id + "/likes", new Bundle(), "POST",
new AsyncRequestListener() {
public void onComplete(JSONObject response, final Object state) {
callJs("javascript:onLike('" + post_id + "'," + val + ")");
}
}, null);
}
public void postComment(final String post_id, final String message) {
Bundle params = new Bundle();
params.putString("message", message);
getFb().request(post_id + "/comments", params, "POST",
new AsyncRequestListener() {
public void onComplete(JSONObject response, final Object state) {
try {
String html = renderComment(response, message);
html = html.replace("'", "\\'");
callJs("onComment('" + post_id + "','" + html + "');");
} catch (JSONException e) {
e.printStackTrace();
}
}
}, null);
}
/**
* Renders the html string for a new comment.
*
* @param response
* @param message
* @return
* @throws JSONException
*/
private String renderComment(JSONObject response, String message)
throws JSONException {
JSONObject comment = new JSONObject();
String commentId = response.getString("id");
comment.put("id", commentId);
comment.put("from", createAuthorObj());
comment.put("message", message);
String html = StreamRenderer.renderSingleComment(comment);
return html;
}
/**
* Executes javascript code inside WebKit.
*
* @param js
*/
private void callJs(String js) {
streamHandler.getWebView().loadUrl("javascript:" + js);
}
/**
* Creates a JSONObject for the post or comment author.
*
* @return
* @throws JSONException
*/
private JSONObject createAuthorObj() throws JSONException {
Session session = Session.restore(streamHandler.getActivity());
JSONObject from = new JSONObject();
from.put("id", session.getUid());
from.put("name", session.getName());
return from;
}
}

@ -0,0 +1,538 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.stream;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;
/**
* Contains logic for converting a JSONObject obtained from
* querying /me/home to a HTML string that can be rendered
* in WebKit.
*
* @author yariv
*/
class StreamRenderer {
private StringBuilder sb;
/**
* The main function for rendering the stream JSONObject.
*
* @param data
* @return
*/
public static String render(JSONObject data) {
StreamRenderer renderer = new StreamRenderer();
return renderer.doRender(data);
}
/**
* Renders the HTML for a single post.
*
* @param post
* @return
* @throws JSONException
*/
public static String renderSinglePost(JSONObject post)
throws JSONException {
StreamRenderer renderer = new StreamRenderer();
renderer.renderPost(post);
return renderer.getResult();
}
/**
* Renders the HTML for a single comment.
*
* @param comment
* @return
*/
public static String renderSingleComment(JSONObject comment) {
StreamRenderer renderer = new StreamRenderer();
renderer.renderComment(comment);
return renderer.getResult();
}
private StreamRenderer() {
this.sb = new StringBuilder();
}
/**
* Returns a SimpleDateFormat object we use for
* parsing and rendering timestamps.
*
* @return
*/
public static SimpleDateFormat getDateFormat() {
return new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZ");
}
/**
* Returns the result html.
*
* @return
*/
private String getResult() {
return sb.toString();
}
private String doRender(JSONObject data) {
try {
JSONArray posts = data.getJSONArray("data");
String[] chunks = {
"<html><head>",
"<link rel=\"stylesheet\" " +
"href=\"file:///android_asset/stream.css\" type=\"text/css\">",
"<script src=\"file:///android_asset/stream.js\"></script>",
"</head>",
"<body>",
"<div id=\"header\">"
};
append(chunks);
renderLink("app://logout", "logout");
renderStatusBox();
append("<div id=\"posts\">");
for (int i = 0; i < posts.length(); i++) {
renderPost(posts.getJSONObject(i));
}
append("</div></body></html>");
return getResult();
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return "";
}
}
/**
* Renders the "what's on your mind?" box and the Share button.
*/
private void renderStatusBox() {
String[] chunks = new String[] {
"</div><div class=\"clear\"></div>",
"<div id=\"status_box\">",
"<input id=\"status_input\" value=\" What's on your mind?\"",
" onfocus=\"onStatusBoxFocus(this);\"/>",
"<button id=\"status_submit\" class=\"hidden\" " +
"onclick=\"updateStatus();\">Share</button>",
"<div class=\"clear\"></div>",
"</div>"
};
append(chunks);
}
/**
* Renders a single post
*
* @param post
* @throws JSONException
*/
private void renderPost(JSONObject post) throws JSONException {
append("<div class=\"post\">");
renderFrom(post);
renderTo(post);
renderMessage(post);
renderAttachment(post);
renderActionLinks(post);
renderLikes(post);
renderComments(post);
renderCommentBox(post);
append("</div>");
}
/**
* Renders the author's name
*
* @param post
* @throws JSONException
*/
private void renderFrom(JSONObject post) throws JSONException {
JSONObject from = post.getJSONObject("from");
String fromName = from.getString("name");
String fromId = from.getString("id");
renderAuthor(fromId, fromName);
}
/**
* If it's a wall post on a friend's fall, renders
* the recipient's name preceded by a '>'.
*
* @param post
* @throws JSONException
*/
private void renderTo(JSONObject post) throws JSONException {
JSONObject to = post.optJSONObject("to");
if (to != null) {
JSONObject toData = to.getJSONArray("data").getJSONObject(0);
String toName = toData.getString("name");
String toId = toData.getString("id");
append(" > ");
renderProfileLink(toId, toName);
}
}
/**
* Renders a link to a user.
*
* @param id
* @param name
*/
private void renderProfileLink(String id, String name) {
renderLink(getProfileUrl(id), name);
}
private String getProfileUrl(String id) {
return "http://touch.facebook.com/#/profile.php?id=" + id;
}
/**
* Renders the author pic and name.
*
* @param id
* @param name
*/
private void renderAuthor(String id, String name) {
String[] chunks = {
"<div class=\"profile_pic_container\">",
"<a href=\"", getProfileUrl(id),
"\"><img class=\"profile_pic\" src=\"http://graph.facebook.com/",
id, "/picture\"/></a>",
"</div>"
};
append(chunks);
renderProfileLink(id, name);
}
/**
* Renders the post message.
*
* @param post
*/
private void renderMessage(JSONObject post) {
String message = post.optString("message");
String[] chunks = {
"&nbsp;<span class=\"msg\">", message, "</span>",
"<div class=\"clear\"></div>"
};
append(chunks);
}
/**
* Renders the attachment.
*
* @param post
*/
private void renderAttachment(JSONObject post) {
String name = post.optString("name");
String link = post.optString("link");
String picture = post.optString("picture");
String source = post.optString("source"); // for videos
String caption = post.optString("caption");
String description = post.optString("description");
String[] fields = new String[] {
name, link, picture, source, caption, description
};
boolean hasAttachment = false;
for (String field : fields) {
if (field.length() != 0) {
hasAttachment = true;
break;
}
}
if (!hasAttachment) {
return;
}
append("<div class=\"attachment\">");
if (name != "") {
append("<div class=\"title\">");
if (link != null) {
renderLink(link, name);
} else {
append(name);
}
append("</div>");
}
if (caption != "") {
append("<div class=\"caption\">" + caption + "</div>");
}
if (picture != "") {
append("<div class=\"picture\">");
String img = "<img src=\"" + picture + "\"/>";
if (link != "") {
renderLink(link, img);
} else {
append(img);
}
append("</div>");
}
if (description != "") {
append("<div class=\"description\">" + description + "</div>");
}
append("<div class=\"clear\"></div></div>");
}
/**
* Renders an anchor tag
*
* @param href
* @param text
*/
private void renderLink(String href, String text) {
append(new String[] {
"<a href=\"",
href,
"\">",
text,
"</a>"
});
}
/**
* Renders the posts' action links.
*
* @param post
*/
private void renderActionLinks(JSONObject post) {
HashSet<String> actions = getActions(post);
append("<div class=\"action_links\">");
append("<div class=\"action_link\">");
renderTimeStamp(post);
append("</div>");
String post_id = post.optString("id");
if (actions.contains("Comment")) {
renderActionLink(post_id, "Comment", "comment");
}
boolean canLike = actions.contains("Like");
renderActionLink(post_id, "Like", "like", canLike);
renderActionLink(post_id, "Unlike", "unlike", !canLike);
append("<div class=\"clear\"></div></div>");
}
/**
* Renders a single visible action link.
*
* @param post_id
* @param title
* @param func
*/
private void renderActionLink(String post_id, String title, String func) {
renderActionLink(post_id, title, func, true);
}
/**
* Renders an action link with optional visibility.
*
* @param post_id
* @param title
* @param func
* @param visible
*/
private void renderActionLink(String post_id, String title, String func,
boolean visible) {
String extraClass = visible ? "" : "hidden";
String[] chunks = new String[] {
"<div id=\"", func, post_id, "\" class=\"action_link ",
extraClass, "\">", "<a href=\"#\" onclick=\"",func, "('",
post_id, "'); return false;\">", title, "</a></div>"
};
append(chunks);
}
/**
* Renders the post's timestamp.
*
* @param post
*/
private void renderTimeStamp(JSONObject post) {
String dateStr = post.optString("created_time");
SimpleDateFormat formatter = getDateFormat();
ParsePosition pos = new ParsePosition(0);
long then = formatter.parse(dateStr, pos).getTime();
long now = new Date().getTime();
long seconds = (now - then)/1000;
long minutes = seconds/60;
long hours = minutes/60;
long days = hours/24;
String friendly = null;
long num = 0;
if (days > 0) {
num = days;
friendly = days + " day";
} else if (hours > 0) {
num = hours;
friendly = hours + " hour";
} else if (minutes > 0) {
num = minutes;
friendly = minutes + " minute";
} else {
num = seconds;
friendly = seconds + " second";
}
if (num > 1) {
friendly += "s";
}
String[] chunks = new String[] {
"<div class=\"timestamp\">",
friendly,
" ago",
"</div>"
};
append(chunks);
}
/**
* Returns the available actions for the post.
*
* @param post
* @return
*/
private HashSet<String> getActions(JSONObject post) {
HashSet<String> actionsSet = new HashSet<String>();
JSONArray actions = post.optJSONArray("actions");
if (actions != null) {
for (int j = 0; j < actions.length(); j++) {
JSONObject action = actions.optJSONObject(j);
String actionName = action.optString("name");
actionsSet.add(actionName);
}
}
return actionsSet;
}
/**
* Renders the 'x people like this' text,
*
* @param post
*/
private void renderLikes(JSONObject post) {
int numLikes = post.optInt("likes", 0);
if (numLikes > 0) {
String desc = numLikes == 1 ?
"person likes this" :
"people like this";
String[] chunks = new String[] {
"<div class=\"like_icon\">",
"<img src=\"file:///android_asset/like_icon.png\"/>",
"</div>",
"<div class=\"num_likes\">",
new Integer(numLikes).toString(),
" ",
desc,
"</div>"
};
append(chunks);
}
}
/**
* Renders the post's comments.
*
* @param post
* @throws JSONException
*/
private void renderComments(JSONObject post) throws JSONException {
append("<div class=\"comments\" id=\"comments" + post.optString("id")
+ "\">");
JSONObject comments = post.optJSONObject("comments");
if (comments != null) {
JSONArray data = comments.optJSONArray("data");
if (data != null) {
for (int j = 0; j < data.length(); j++) {
JSONObject comment = data.getJSONObject(j);
renderComment(comment);
}
}
}
append("</div>");
}
/**
* Renders an individual comment.
*
* @param comment
*/
private void renderComment(JSONObject comment) {
JSONObject from = comment.optJSONObject("from");
if (from == null) {
Log.w("StreamRenderer",
"Comment missing from field: " + comment.toString());
} else {
String authorId = from.optString("id");
String authorName = from.optString("name");
renderAuthor(authorId, authorName);
}
String message = comment.optString("message");
append("<div class=\"comment\">");
String[] chunks = {
"&nbsp;",
message,
"</div>"
};
append(chunks);
}
/**
* Renders the new comment input box.
*
* @param post
*/
private void renderCommentBox(JSONObject post) {
String id = post.optString("id");
String[] chunks = new String[] {
"<div class=\"comment_box\" id=\"comment_box", id, "\">",
"<input id=\"comment_box_input", id, "\"/>",
"<button onclick=\"postComment('", id , "');\">Post</button>",
"<div class=\"clear\"></div>",
"</div>"
};
append(chunks);
}
private void append(String str) {
sb.append(str);
}
private void append(String[] chunks) {
for (String chunk : chunks) {
sb.append(chunk);
}
}
}

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
* Copyright (C) 2010 Facebook Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.android">
<application/>
</manifest>

@ -0,0 +1,12 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
android.library=true
# Project target.
target=android-3

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

@ -0,0 +1,316 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import android.content.Context;
import android.os.Bundle;
/**
* A sample implementation of asynchronous API requests. This class provides
* the ability to execute API methods and have the call return immediately,
* without blocking the calling thread. This is necessary when accessing the
* API in the UI thread, for instance. The request response is returned to
* the caller via a callback interface, which the developer must implement.
*
* This sample implementation simply spawns a new thread for each request,
* and makes the API call immediately. This may work in many applications,
* but more sophisticated users may re-implement this behavior using a thread
* pool, a network thread, a request queue, or other mechanism. Advanced
* functionality could be built, such as rate-limiting of requests, as per
* a specific application's needs.
*
* @see RequestListener
* The callback interface.
*
* @author Jim Brusstar (jimbru@fb.com),
* Yariv Sadan (yariv@fb.com),
* Luke Shepard (lshepard@fb.com)
*/
public class AsyncFacebookRunner {
Facebook fb;
public AsyncFacebookRunner(Facebook fb) {
this.fb = fb;
}
/**
* Invalidate the current user session by removing the access token in
* memory, clearing the browser cookies, and calling auth.expireSession
* through the API. The application will be notified when logout is
* complete via the callback interface.
*
* Note that this method is asynchronous and the callback will be invoked
* in a background thread; operations that affect the UI will need to be
* posted to the UI thread or an appropriate handler.
*
* @param context
* The Android context in which the logout should be called: it
* should be the same context in which the login occurred in
* order to clear any stored cookies
* @param listener
* Callback interface to notify the application when the request
* has completed.
* @param state
* An arbitrary object used to identify the request when it
* returns to the callback. This has no effect on the request
* itself.
*/
public void logout(final Context context,
final RequestListener listener,
final Object state) {
new Thread() {
@Override public void run() {
try {
String response = fb.logout(context);
if (response.length() == 0 || response.equals("false")){
listener.onFacebookError(new FacebookError(
"auth.expireSession failed"), state);
return;
}
listener.onComplete(response, state);
} catch (FileNotFoundException e) {
listener.onFileNotFoundException(e, state);
} catch (MalformedURLException e) {
listener.onMalformedURLException(e, state);
} catch (IOException e) {
listener.onIOException(e, state);
}
}
}.start();
}
public void logout(final Context context, final RequestListener listener) {
logout(context, listener, /* state */ null);
}
/**
* Make a request to Facebook's old (pre-graph) API with the given
* parameters. One of the parameter keys must be "method" and its value
* should be a valid REST server API method.
*
* See http://developers.facebook.com/docs/reference/rest/
*
* Note that this method is asynchronous and the callback will be invoked
* in a background thread; operations that affect the UI will need to be
* posted to the UI thread or an appropriate handler.
*
* Example:
* <code>
* Bundle parameters = new Bundle();
* parameters.putString("method", "auth.expireSession", new Listener());
* String response = request(parameters);
* </code>
*
* @param parameters
* Key-value pairs of parameters to the request. Refer to the
* documentation: one of the parameters must be "method".
* @param listener
* Callback interface to notify the application when the request
* has completed.
* @param state
* An arbitrary object used to identify the request when it
* returns to the callback. This has no effect on the request
* itself.
*/
public void request(Bundle parameters,
RequestListener listener,
final Object state) {
request(null, parameters, "GET", listener, state);
}
public void request(Bundle parameters, RequestListener listener) {
request(null, parameters, "GET", listener, /* state */ null);
}
/**
* Make a request to the Facebook Graph API without any parameters.
*
* See http://developers.facebook.com/docs/api
*
* Note that this method is asynchronous and the callback will be invoked
* in a background thread; operations that affect the UI will need to be
* posted to the UI thread or an appropriate handler.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @param listener
* Callback interface to notify the application when the request
* has completed.
* @param state
* An arbitrary object used to identify the request when it
* returns to the callback. This has no effect on the request
* itself.
*/
public void request(String graphPath,
RequestListener listener,
final Object state) {
request(graphPath, new Bundle(), "GET", listener, state);
}
public void request(String graphPath, RequestListener listener) {
request(graphPath, new Bundle(), "GET", listener, /* state */ null);
}
/**
* Make a request to the Facebook Graph API with the given string parameters
* using an HTTP GET (default method).
*
* See http://developers.facebook.com/docs/api
*
* Note that this method is asynchronous and the callback will be invoked
* in a background thread; operations that affect the UI will need to be
* posted to the UI thread or an appropriate handler.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @param parameters
* key-value string parameters, e.g. the path "search" with
* parameters "q" : "facebook" would produce a query for the
* following graph resource:
* https://graph.facebook.com/search?q=facebook
* @param listener
* Callback interface to notify the application when the request
* has completed.
* @param state
* An arbitrary object used to identify the request when it
* returns to the callback. This has no effect on the request
* itself.
*/
public void request(String graphPath,
Bundle parameters,
RequestListener listener,
final Object state) {
request(graphPath, parameters, "GET", listener, state);
}
public void request(String graphPath,
Bundle parameters,
RequestListener listener) {
request(graphPath, parameters, "GET", listener, /* state */ null);
}
/**
* Make a request to the Facebook Graph API with the given HTTP method and
* string parameters. Note that binary data parameters (e.g. pictures) are
* not yet supported by this helper function.
*
* See http://developers.facebook.com/docs/api
*
* Note that this method is asynchronous and the callback will be invoked
* in a background thread; operations that affect the UI will need to be
* posted to the UI thread or an appropriate handler.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @param parameters
* key-value string parameters, e.g. the path "search" with
* parameters {"q" : "facebook"} would produce a query for the
* following graph resource:
* https://graph.facebook.com/search?q=facebook
* @param httpMethod
* http verb, e.g. "POST", "DELETE"
* @param listener
* Callback interface to notify the application when the request
* has completed.
* @param state
* An arbitrary object used to identify the request when it
* returns to the callback. This has no effect on the request
* itself.
*/
public void request(final String graphPath,
final Bundle parameters,
final String httpMethod,
final RequestListener listener,
final Object state) {
new Thread() {
@Override public void run() {
try {
String resp = fb.request(graphPath, parameters, httpMethod);
listener.onComplete(resp, state);
} catch (FileNotFoundException e) {
listener.onFileNotFoundException(e, state);
} catch (MalformedURLException e) {
listener.onMalformedURLException(e, state);
} catch (IOException e) {
listener.onIOException(e, state);
}
}
}.start();
}
/**
* Callback interface for API requests.
*
* Each method includes a 'state' parameter that identifies the calling
* request. It will be set to the value passed when originally calling the
* request method, or null if none was passed.
*/
public static interface RequestListener {
/**
* Called when a request completes with the given response.
*
* Executed by a background thread: do not update the UI in this method.
*/
public void onComplete(String response, Object state);
/**
* Called when a request has a network or request error.
*
* Executed by a background thread: do not update the UI in this method.
*/
public void onIOException(IOException e, Object state);
/**
* Called when a request fails because the requested resource is
* invalid or does not exist.
*
* Executed by a background thread: do not update the UI in this method.
*/
public void onFileNotFoundException(FileNotFoundException e,
Object state);
/**
* Called if an invalid graph path is provided (which may result in a
* malformed URL).
*
* Executed by a background thread: do not update the UI in this method.
*/
public void onMalformedURLException(MalformedURLException e,
Object state);
/**
* Called when the server-side Facebook method fails.
*
* Executed by a background thread: do not update the UI in this method.
*/
public void onFacebookError(FacebookError e, Object state);
}
}

@ -0,0 +1,51 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
/**
* Encapsulation of Dialog Error.
*
* @author ssoneff@facebook.com
*/
public class DialogError extends Throwable {
private static final long serialVersionUID = 1L;
/**
* The ErrorCode received by the WebView: see
* http://developer.android.com/reference/android/webkit/WebViewClient.html
*/
private int mErrorCode;
/** The URL that the dialog was trying to load */
private String mFailingUrl;
public DialogError(String message, int errorCode, String failingUrl) {
super(message);
mErrorCode = errorCode;
mFailingUrl = failingUrl;
}
int getErrorCode() {
return mErrorCode;
}
String getFailingUrl() {
return mFailingUrl;
}
}

@ -0,0 +1,761 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.CookieSyncManager;
/**
* Main Facebook object for interacting with the Facebook developer API.
* Provides methods to log in and log out a user, make requests using the REST
* and Graph APIs, and start user interface interactions with the API (such as
* pop-ups promoting for credentials, permissions, stream posts, etc.)
*
* @author Jim Brusstar (jimbru@facebook.com),
* Yariv Sadan (yariv@facebook.com),
* Luke Shepard (lshepard@facebook.com)
*/
public class Facebook {
// Strings used in the authorization flow
public static final String REDIRECT_URI = "fbconnect://success";
public static final String CANCEL_URI = "fbconnect://cancel";
public static final String TOKEN = "access_token";
public static final String EXPIRES = "expires_in";
public static final String SINGLE_SIGN_ON_DISABLED = "service_disabled";
public static final int FORCE_DIALOG_AUTH = -1;
private static final String LOGIN = "oauth";
// Used as default activityCode by authorize(). See authorize() below.
private static final int DEFAULT_AUTH_ACTIVITY_CODE = 32665;
// Facebook server endpoints: may be modified in a subclass for testing
protected static String DIALOG_BASE_URL =
"https://m.facebook.com/dialog/";
protected static String GRAPH_BASE_URL =
"https://graph.facebook.com/";
protected static String RESTSERVER_URL =
"https://api.facebook.com/restserver.php";
private String mAccessToken = null;
private long mAccessExpires = 0;
private String mAppId;
private Activity mAuthActivity;
private String[] mAuthPermissions;
private int mAuthActivityCode;
private DialogListener mAuthDialogListener;
/**
* Constructor for Facebook object.
*
* @param appId
* Your Facebook application ID. Found at
* www.facebook.com/developers/apps.php.
*/
public Facebook(String appId) {
if (appId == null) {
throw new IllegalArgumentException(
"You must specify your application ID when instantiating " +
"a Facebook object. See README for details.");
}
mAppId = appId;
}
/**
* Default authorize method. Grants only basic permissions.
*
* See authorize() below for @params.
*/
public void authorize(Activity activity, final DialogListener listener) {
authorize(activity, new String[] {}, DEFAULT_AUTH_ACTIVITY_CODE,
listener);
}
/**
* Authorize method that grants custom permissions.
*
* See authorize() below for @params.
*/
public void authorize(Activity activity, String[] permissions,
final DialogListener listener) {
authorize(activity, permissions, DEFAULT_AUTH_ACTIVITY_CODE, listener);
}
/**
* Full authorize method.
*
* Starts either an Activity or a dialog which prompts the user to log in to
* Facebook and grant the requested permissions to the given application.
*
* This method will, when possible, use Facebook's single sign-on for
* Android to obtain an access token. This involves proxying a call through
* the Facebook for Android stand-alone application, which will handle the
* authentication flow, and return an OAuth access token for making API
* calls.
*
* Because this process will not be available for all users, if single
* sign-on is not possible, this method will automatically fall back to the
* OAuth 2.0 User-Agent flow. In this flow, the user credentials are handled
* by Facebook in an embedded WebView, not by the client application. As
* such, the dialog makes a network request and renders HTML content rather
* than a native UI. The access token is retrieved from a redirect to a
* special URL that the WebView handles.
*
* Note that User credentials could be handled natively using the OAuth 2.0
* Username and Password Flow, but this is not supported by this SDK.
*
* See http://developers.facebook.com/docs/authentication/ and
* http://wiki.oauth.net/OAuth-2 for more details.
*
* Note that this method is asynchronous and the callback will be invoked in
* the original calling thread (not in a background thread).
*
* Also note that requests may be made to the API without calling authorize
* first, in which case only public information is returned.
*
* IMPORTANT: Note that single sign-on authentication will not function
* correctly if you do not include a call to the authorizeCallback() method
* in your onActivityResult() function! Please see below for more
* information. single sign-on may be disabled by passing FORCE_DIALOG_AUTH
* as the activityCode parameter in your call to authorize().
*
* @param activity
* The Android activity in which we want to display the
* authorization dialog.
* @param applicationId
* The Facebook application identifier e.g. "350685531728"
* @param permissions
* A list of permissions required for this application: e.g.
* "read_stream", "publish_stream", "offline_access", etc. see
* http://developers.facebook.com/docs/authentication/permissions
* This parameter should not be null -- if you do not require any
* permissions, then pass in an empty String array.
* @param activityCode
* Single sign-on requires an activity result to be called back
* to the client application -- if you are waiting on other
* activities to return data, pass a custom activity code here to
* avoid collisions. If you would like to force the use of legacy
* dialog-based authorization, pass FORCE_DIALOG_AUTH for this
* parameter. Otherwise just omit this parameter and Facebook
* will use a suitable default. See
* http://developer.android.com/reference/android/
* app/Activity.html for more information.
* @param listener
* Callback interface for notifying the calling application when
* the authentication dialog has completed, failed, or been
* canceled.
*/
public void authorize(Activity activity, String[] permissions,
int activityCode, final DialogListener listener) {
boolean singleSignOnStarted = false;
mAuthDialogListener = listener;
// Prefer single sign-on, where available.
if (activityCode >= 0) {
singleSignOnStarted = startSingleSignOn(activity, mAppId,
permissions, activityCode);
}
// Otherwise fall back to traditional dialog.
if (!singleSignOnStarted) {
startDialogAuth(activity, permissions);
}
}
/**
* Internal method to handle single sign-on backend for authorize().
*
* @param activity
* The Android Activity that will parent the ProxyAuth Activity.
* @param applicationId
* The Facebook application identifier.
* @param permissions
* A list of permissions required for this application. If you do
* not require any permissions, pass an empty String array.
* @param activityCode
* Activity code to uniquely identify the result Intent in the
* callback.
*/
private boolean startSingleSignOn(Activity activity, String applicationId,
String[] permissions, int activityCode) {
boolean didSucceed = true;
Intent intent = new Intent();
intent.setClassName("com.facebook.katana",
"com.facebook.katana.ProxyAuth");
intent.putExtra("client_id", applicationId);
if (permissions.length > 0) {
intent.putExtra("scope", TextUtils.join(",", permissions));
}
// Verify that the application whose package name is
// com.facebook.katana.ProxyAuth
// has the expected FB app signature.
if (!validateAppSignatureForIntent(activity, intent)) {
return false;
}
mAuthActivity = activity;
mAuthPermissions = permissions;
mAuthActivityCode = activityCode;
try {
activity.startActivityForResult(intent, activityCode);
} catch (ActivityNotFoundException e) {
didSucceed = false;
}
return didSucceed;
}
/**
* Query the signature for the application that would be invoked by the
* given intent and verify that it matches the FB application's signature.
*
* @param activity
* @param intent
* @param validSignature
* @return true if the app's signature matches the expected signature.
*/
private boolean validateAppSignatureForIntent(Activity activity,
Intent intent) {
ResolveInfo resolveInfo =
activity.getPackageManager().resolveActivity(intent, 0);
if (resolveInfo == null) {
return false;
}
String packageName = resolveInfo.activityInfo.packageName;
PackageInfo packageInfo;
try {
packageInfo = activity.getPackageManager().getPackageInfo(
packageName, PackageManager.GET_SIGNATURES);
} catch (NameNotFoundException e) {
return false;
}
for (Signature signature : packageInfo.signatures) {
if (signature.toCharsString().equals(FB_APP_SIGNATURE)) {
return true;
}
}
return false;
}
/**
* Internal method to handle dialog-based authentication backend for
* authorize().
*
* @param activity
* The Android Activity that will parent the auth dialog.
* @param applicationId
* The Facebook application identifier.
* @param permissions
* A list of permissions required for this application. If you do
* not require any permissions, pass an empty String array.
*/
private void startDialogAuth(Activity activity, String[] permissions) {
Bundle params = new Bundle();
if (permissions.length > 0) {
params.putString("scope", TextUtils.join(",", permissions));
}
CookieSyncManager.createInstance(activity);
dialog(activity, LOGIN, params, new DialogListener() {
public void onComplete(Bundle values) {
// ensure any cookies set by the dialog are saved
CookieSyncManager.getInstance().sync();
setAccessToken(values.getString(TOKEN));
setAccessExpiresIn(values.getString(EXPIRES));
if (isSessionValid()) {
Log.d("Facebook-authorize", "Login Success! access_token="
+ getAccessToken() + " expires="
+ getAccessExpires());
mAuthDialogListener.onComplete(values);
} else {
mAuthDialogListener.onFacebookError(new FacebookError(
"Failed to receive access token."));
}
}
public void onError(DialogError error) {
Log.d("Facebook-authorize", "Login failed: " + error);
mAuthDialogListener.onError(error);
}
public void onFacebookError(FacebookError error) {
Log.d("Facebook-authorize", "Login failed: " + error);
mAuthDialogListener.onFacebookError(error);
}
public void onCancel() {
Log.d("Facebook-authorize", "Login canceled");
mAuthDialogListener.onCancel();
}
});
}
/**
* IMPORTANT: This method must be invoked at the top of the calling
* activity's onActivityResult() function or Facebook authentication will
* not function properly!
*
* If your calling activity does not currently implement onActivityResult(),
* you must implement it and include a call to this method if you intend to
* use the authorize() method in this SDK.
*
* For more information, see
* http://developer.android.com/reference/android/app/
* Activity.html#onActivityResult(int, int, android.content.Intent)
*/
public void authorizeCallback(int requestCode, int resultCode, Intent data) {
if (requestCode == mAuthActivityCode) {
// Successfully redirected.
if (resultCode == Activity.RESULT_OK) {
// Check OAuth 2.0/2.10 error code.
String error = data.getStringExtra("error");
if (error == null) {
error = data.getStringExtra("error_type");
}
// A Facebook error occurred.
if (error != null) {
if (error.equals(SINGLE_SIGN_ON_DISABLED)
|| error.equals("AndroidAuthKillSwitchException")) {
Log.d("Facebook-authorize", "Hosted auth currently "
+ "disabled. Retrying dialog auth...");
startDialogAuth(mAuthActivity, mAuthPermissions);
} else if (error.equals("access_denied")
|| error.equals("OAuthAccessDeniedException")) {
Log.d("Facebook-authorize", "Login canceled by user.");
mAuthDialogListener.onCancel();
} else {
Log.d("Facebook-authorize", "Login failed: " + error);
mAuthDialogListener.onFacebookError(
new FacebookError(error));
}
// No errors.
} else {
setAccessToken(data.getStringExtra(TOKEN));
setAccessExpiresIn(data.getStringExtra(EXPIRES));
if (isSessionValid()) {
Log.d("Facebook-authorize",
"Login Success! access_token="
+ getAccessToken() + " expires="
+ getAccessExpires());
mAuthDialogListener.onComplete(data.getExtras());
} else {
mAuthDialogListener.onFacebookError(new FacebookError(
"Failed to receive access token."));
}
}
// An error occurred before we could be redirected.
} else if (resultCode == Activity.RESULT_CANCELED) {
// An Android error occured.
if (data != null) {
Log.d("Facebook-authorize",
"Login failed: " + data.getStringExtra("error"));
mAuthDialogListener.onError(
new DialogError(
data.getStringExtra("error"),
data.getIntExtra("error_code", -1),
data.getStringExtra("failing_url")));
// User pressed the 'back' button.
} else {
Log.d("Facebook-authorize", "Login canceled by user.");
mAuthDialogListener.onCancel();
}
}
}
}
/**
* Invalidate the current user session by removing the access token in
* memory, clearing the browser cookie, and calling auth.expireSession
* through the API.
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* @param context
* The Android context in which the logout should be called: it
* should be the same context in which the login occurred in
* order to clear any stored cookies
* @throws IOException
* @throws MalformedURLException
* @return JSON string representation of the auth.expireSession response
* ("true" if successful)
*/
public String logout(Context context)
throws MalformedURLException, IOException {
Util.clearCookies(context);
Bundle b = new Bundle();
b.putString("method", "auth.expireSession");
String response = request(b);
setAccessToken(null);
setAccessExpires(0);
return response;
}
/**
* Make a request to Facebook's old (pre-graph) API with the given
* parameters. One of the parameter keys must be "method" and its value
* should be a valid REST server API method.
*
* See http://developers.facebook.com/docs/reference/rest/
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* Example:
* <code>
* Bundle parameters = new Bundle();
* parameters.putString("method", "auth.expireSession");
* String response = request(parameters);
* </code>
*
* @param parameters
* Key-value pairs of parameters to the request. Refer to the
* documentation: one of the parameters must be "method".
* @throws IOException
* if a network error occurs
* @throws MalformedURLException
* if accessing an invalid endpoint
* @throws IllegalArgumentException
* if one of the parameters is not "method"
* @return JSON string representation of the response
*/
public String request(Bundle parameters)
throws MalformedURLException, IOException {
if (!parameters.containsKey("method")) {
throw new IllegalArgumentException("API method must be specified. "
+ "(parameters must contain key \"method\" and value). See"
+ " http://developers.facebook.com/docs/reference/rest/");
}
return request(null, parameters, "GET");
}
/**
* Make a request to the Facebook Graph API without any parameters.
*
* See http://developers.facebook.com/docs/api
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @throws IOException
* @throws MalformedURLException
* @return JSON string representation of the response
*/
public String request(String graphPath)
throws MalformedURLException, IOException {
return request(graphPath, new Bundle(), "GET");
}
/**
* Make a request to the Facebook Graph API with the given string parameters
* using an HTTP GET (default method).
*
* See http://developers.facebook.com/docs/api
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @param parameters
* key-value string parameters, e.g. the path "search" with
* parameters "q" : "facebook" would produce a query for the
* following graph resource:
* https://graph.facebook.com/search?q=facebook
* @throws IOException
* @throws MalformedURLException
* @return JSON string representation of the response
*/
public String request(String graphPath, Bundle parameters)
throws MalformedURLException, IOException {
return request(graphPath, parameters, "GET");
}
/**
* Synchronously make a request to the Facebook Graph API with the given
* HTTP method and string parameters. Note that binary data parameters
* (e.g. pictures) are not yet supported by this helper function.
*
* See http://developers.facebook.com/docs/api
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @param params
* Key-value string parameters, e.g. the path "search" with
* parameters {"q" : "facebook"} would produce a query for the
* following graph resource:
* https://graph.facebook.com/search?q=facebook
* @param httpMethod
* http verb, e.g. "GET", "POST", "DELETE"
* @throws IOException
* @throws MalformedURLException
* @return JSON string representation of the response
*/
public String request(String graphPath, Bundle params, String httpMethod)
throws FileNotFoundException, MalformedURLException, IOException {
params.putString("format", "json");
if (isSessionValid()) {
params.putString(TOKEN, getAccessToken());
}
String url = (graphPath != null) ? GRAPH_BASE_URL + graphPath
: RESTSERVER_URL;
return Util.openUrl(url, httpMethod, params);
}
/**
* Generate a UI dialog for the request action in the given Android context.
*
* Note that this method is asynchronous and the callback will be invoked in
* the original calling thread (not in a background thread).
*
* @param context
* The Android context in which we will generate this dialog.
* @param action
* String representation of the desired method: e.g. "login",
* "stream.publish", ...
* @param listener
* Callback interface to notify the application when the dialog
* has completed.
*/
public void dialog(Context context, String action,
DialogListener listener) {
dialog(context, action, new Bundle(), listener);
}
/**
* Generate a UI dialog for the request action in the given Android context
* with the provided parameters.
*
* Note that this method is asynchronous and the callback will be invoked in
* the original calling thread (not in a background thread).
*
* @param context
* The Android context in which we will generate this dialog.
* @param action
* String representation of the desired method: e.g. "feed" ...
* @param parameters
* String key-value pairs to be passed as URL parameters.
* @param listener
* Callback interface to notify the application when the dialog
* has completed.
*/
public void dialog(Context context, String action, Bundle parameters,
final DialogListener listener) {
String endpoint = DIALOG_BASE_URL + action;
parameters.putString("display", "touch");
parameters.putString("redirect_uri", REDIRECT_URI);
if (action.equals(LOGIN)) {
parameters.putString("type", "user_agent");
parameters.putString("client_id", mAppId);
} else {
parameters.putString("app_id", mAppId);
}
if (isSessionValid()) {
parameters.putString(TOKEN, getAccessToken());
}
String url = endpoint + "?" + Util.encodeUrl(parameters);
if (context.checkCallingOrSelfPermission(Manifest.permission.INTERNET)
!= PackageManager.PERMISSION_GRANTED) {
Util.showAlert(context, "Error",
"Application requires permission to access the Internet");
} else {
new FbDialog(context, url, listener).show();
}
}
/**
* @return boolean - whether this object has an non-expired session token
*/
public boolean isSessionValid() {
return (getAccessToken() != null) &&
((getAccessExpires() == 0) ||
(System.currentTimeMillis() < getAccessExpires()));
}
/**
* Retrieve the OAuth 2.0 access token for API access: treat with care.
* Returns null if no session exists.
*
* @return String - access token
*/
public String getAccessToken() {
return mAccessToken;
}
/**
* Retrieve the current session's expiration time (in milliseconds since
* Unix epoch), or 0 if the session doesn't expire or doesn't exist.
*
* @return long - session expiration time
*/
public long getAccessExpires() {
return mAccessExpires;
}
/**
* Set the OAuth 2.0 access token for API access.
*
* @param token
* - access token
*/
public void setAccessToken(String token) {
mAccessToken = token;
}
/**
* Set the current session's expiration time (in milliseconds since Unix
* epoch), or 0 if the session doesn't expire.
*
* @param time
* - timestamp in milliseconds
*/
public void setAccessExpires(long time) {
mAccessExpires = time;
}
/**
* Set the current session's duration (in seconds since Unix epoch).
*
* @param expiresIn
* - duration in seconds
*/
public void setAccessExpiresIn(String expiresIn) {
if (expiresIn != null && !expiresIn.equals("0")) {
setAccessExpires(System.currentTimeMillis()
+ Integer.parseInt(expiresIn) * 1000);
}
}
public String getAppId() {
return mAppId;
}
public void setAppId(String appId) {
mAppId = appId;
}
/**
* Callback interface for dialog requests.
*
*/
public static interface DialogListener {
/**
* Called when a dialog completes.
*
* Executed by the thread that initiated the dialog.
*
* @param values
* Key-value string pairs extracted from the response.
*/
public void onComplete(Bundle values);
/**
* Called when a Facebook responds to a dialog with an error.
*
* Executed by the thread that initiated the dialog.
*
*/
public void onFacebookError(FacebookError e);
/**
* Called when a dialog has an error.
*
* Executed by the thread that initiated the dialog.
*
*/
public void onError(DialogError e);
/**
* Called when a dialog is canceled by the user.
*
* Executed by the thread that initiated the dialog.
*
*/
public void onCancel();
}
public static final String FB_APP_SIGNATURE =
"30820268308201d102044a9c4610300d06092a864886f70d0101040500307a310"
+ "b3009060355040613025553310b30090603550408130243413112301006035504"
+ "07130950616c6f20416c746f31183016060355040a130f46616365626f6f6b204"
+ "d6f62696c653111300f060355040b130846616365626f6f6b311d301b06035504"
+ "03131446616365626f6f6b20436f72706f726174696f6e3020170d30393038333"
+ "13231353231365a180f32303530303932353231353231365a307a310b30090603"
+ "55040613025553310b30090603550408130243413112301006035504071309506"
+ "16c6f20416c746f31183016060355040a130f46616365626f6f6b204d6f62696c"
+ "653111300f060355040b130846616365626f6f6b311d301b06035504031314466"
+ "16365626f6f6b20436f72706f726174696f6e30819f300d06092a864886f70d01"
+ "0101050003818d0030818902818100c207d51df8eb8c97d93ba0c8c1002c928fa"
+ "b00dc1b42fca5e66e99cc3023ed2d214d822bc59e8e35ddcf5f44c7ae8ade50d7"
+ "e0c434f500e6c131f4a2834f987fc46406115de2018ebbb0d5a3c261bd97581cc"
+ "fef76afc7135a6d59e8855ecd7eacc8f8737e794c60a761c536b72b11fac8e603"
+ "f5da1a2d54aa103b8a13c0dbc10203010001300d06092a864886f70d010104050"
+ "0038181005ee9be8bcbb250648d3b741290a82a1c9dc2e76a0af2f2228f1d9f9c"
+ "4007529c446a70175c5a900d5141812866db46be6559e2141616483998211f4a6"
+ "73149fb2232a10d247663b26a9031e15f84bc1c74d141ff98a02d76f85b2c8ab2"
+ "571b6469b232d8e768a7f7ca04f7abe4a775615916c07940656b58717457b42bd"
+ "928a2";
}

@ -0,0 +1,50 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
/**
* Encapsulation of a Facebook Error: a Facebook request that could not be
* fulfilled.
*
* @author ssoneff@facebook.com
*/
public class FacebookError extends Throwable {
private static final long serialVersionUID = 1L;
private int mErrorCode = 0;
private String mErrorType;
public FacebookError(String message) {
super(message);
}
public FacebookError(String message, String type, int code) {
super(message);
mErrorType = type;
mErrorCode = code;
}
public int getErrorCode() {
return mErrorCode;
}
public String getErrorType() {
return mErrorType;
}
}

@ -0,0 +1,179 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.ViewGroup;
import android.view.Window;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.facebook.android.Facebook.DialogListener;
public class FbDialog extends Dialog {
static final int FB_BLUE = 0xFF6D84B4;
static final float[] DIMENSIONS_LANDSCAPE = {460, 260};
static final float[] DIMENSIONS_PORTRAIT = {280, 420};
static final FrameLayout.LayoutParams FILL =
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.FILL_PARENT);
static final int MARGIN = 4;
static final int PADDING = 2;
static final String DISPLAY_STRING = "touch";
static final String FB_ICON = "icon.png";
private String mUrl;
private DialogListener mListener;
private ProgressDialog mSpinner;
private WebView mWebView;
private LinearLayout mContent;
private TextView mTitle;
public FbDialog(Context context, String url, DialogListener listener) {
super(context);
mUrl = url;
mListener = listener;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSpinner = new ProgressDialog(getContext());
mSpinner.requestWindowFeature(Window.FEATURE_NO_TITLE);
mSpinner.setMessage("Loading...");
mContent = new LinearLayout(getContext());
mContent.setOrientation(LinearLayout.VERTICAL);
setUpTitle();
setUpWebView();
Display display = getWindow().getWindowManager().getDefaultDisplay();
final float scale = getContext().getResources().getDisplayMetrics().density;
float[] dimensions =
(display.getWidth() < display.getHeight())
? DIMENSIONS_PORTRAIT : DIMENSIONS_LANDSCAPE;
addContentView(mContent, new FrameLayout.LayoutParams(
(int) (dimensions[0] * scale + 0.5f),
(int) (dimensions[1] * scale + 0.5f)));
}
private void setUpTitle() {
requestWindowFeature(Window.FEATURE_NO_TITLE);
Drawable icon = getContext().getResources().getDrawable(
R.drawable.facebook_icon);
mTitle = new TextView(getContext());
mTitle.setText("Facebook");
mTitle.setTextColor(Color.WHITE);
mTitle.setTypeface(Typeface.DEFAULT_BOLD);
mTitle.setBackgroundColor(FB_BLUE);
mTitle.setPadding(MARGIN + PADDING, MARGIN, MARGIN, MARGIN);
mTitle.setCompoundDrawablePadding(MARGIN + PADDING);
mTitle.setCompoundDrawablesWithIntrinsicBounds(
icon, null, null, null);
mContent.addView(mTitle);
}
private void setUpWebView() {
mWebView = new WebView(getContext());
mWebView.setVerticalScrollBarEnabled(false);
mWebView.setHorizontalScrollBarEnabled(false);
mWebView.setWebViewClient(new FbDialog.FbWebViewClient());
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.loadUrl(mUrl);
mWebView.setLayoutParams(FILL);
mContent.addView(mWebView);
}
private class FbWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.d("Facebook-WebView", "Redirect URL: " + url);
if (url.startsWith(Facebook.REDIRECT_URI)) {
Bundle values = Util.parseUrl(url);
String error = values.getString("error");
if (error == null) {
error = values.getString("error_type");
}
if (error == null) {
mListener.onComplete(values);
} else if (error.equals("access_denied") ||
error.equals("OAuthAccessDeniedException")) {
mListener.onCancel();
} else {
mListener.onFacebookError(new FacebookError(error));
}
FbDialog.this.dismiss();
return true;
} else if (url.startsWith(Facebook.CANCEL_URI)) {
mListener.onCancel();
FbDialog.this.dismiss();
return true;
} else if (url.contains(DISPLAY_STRING)) {
return false;
}
// launch non-dialog URLs in a full browser
getContext().startActivity(
new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
mListener.onError(
new DialogError(description, errorCode, failingUrl));
FbDialog.this.dismiss();
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
Log.d("Facebook-WebView", "Webview loading URL: " + url);
super.onPageStarted(view, url, favicon);
mSpinner.show();
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
String title = mWebView.getTitle();
if (title != null && title.length() > 0) {
mTitle.setText(title);
}
mSpinner.dismiss();
}
}
}

@ -0,0 +1,301 @@
/*
* Copyright 2010 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.android;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
/**
* Utility class supporting the Facebook Object.
*
* @author ssoneff@facebook.com
*
*/
public final class Util {
/**
* Generate the multi-part post body providing the parameters and boundary
* string
*
* @param parameters the parameters need to be posted
* @param boundary the random string as boundary
* @return a string of the post body
*/
public static String encodePostBody(Bundle parameters, String boundary) {
if (parameters == null) return "";
StringBuilder sb = new StringBuilder();
for (String key : parameters.keySet()) {
if (parameters.getByteArray(key) != null) {
continue;
}
sb.append("Content-Disposition: form-data; name=\"" + key +
"\"\r\n\r\n" + parameters.getString(key));
sb.append("\r\n" + "--" + boundary + "\r\n");
}
return sb.toString();
}
public static String encodeUrl(Bundle parameters) {
if (parameters == null) {
return "";
}
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String key : parameters.keySet()) {
if (first) first = false; else sb.append("&");
sb.append(URLEncoder.encode(key) + "=" +
URLEncoder.encode(parameters.getString(key)));
}
return sb.toString();
}
public static Bundle decodeUrl(String s) {
Bundle params = new Bundle();
if (s != null) {
String array[] = s.split("&");
for (String parameter : array) {
String v[] = parameter.split("=");
params.putString(URLDecoder.decode(v[0]),
URLDecoder.decode(v[1]));
}
}
return params;
}
/**
* Parse a URL query and fragment parameters into a key-value bundle.
*
* @param url the URL to parse
* @return a dictionary bundle of keys and values
*/
public static Bundle parseUrl(String url) {
// hack to prevent MalformedURLException
url = url.replace("fbconnect", "http");
try {
URL u = new URL(url);
Bundle b = decodeUrl(u.getQuery());
b.putAll(decodeUrl(u.getRef()));
return b;
} catch (MalformedURLException e) {
return new Bundle();
}
}
/**
* Connect to an HTTP URL and return the response as a string.
*
* Note that the HTTP method override is used on non-GET requests. (i.e.
* requests are made as "POST" with method specified in the body).
*
* @param url - the resource to open: must be a welformed URL
* @param method - the HTTP method to use ("GET", "POST", etc.)
* @param params - the query parameter for the URL (e.g. access_token=foo)
* @return the URL contents as a String
* @throws MalformedURLException - if the URL format is invalid
* @throws IOException - if a network problem occurs
*/
public static String openUrl(String url, String method, Bundle params)
throws MalformedURLException, IOException {
// random string as boundary for multi-part http post
String strBoundary = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f";
String endLine = "\r\n";
OutputStream os;
if (method.equals("GET")) {
url = url + "?" + encodeUrl(params);
}
Log.d("Facebook-Util", method + " URL: " + url);
HttpURLConnection conn =
(HttpURLConnection) new URL(url).openConnection();
conn.setRequestProperty("User-Agent", System.getProperties().
getProperty("http.agent") + " FacebookAndroidSDK");
if (!method.equals("GET")) {
Bundle dataparams = new Bundle();
for (String key : params.keySet()) {
if (params.getByteArray(key) != null) {
dataparams.putByteArray(key, params.getByteArray(key));
}
}
// use method override
if (!params.containsKey("method")) {
params.putString("method", method);
}
if (params.containsKey("access_token")) {
String decoded_token =
URLDecoder.decode(params.getString("access_token"));
params.putString("access_token", decoded_token);
}
conn.setRequestMethod("POST");
conn.setRequestProperty(
"Content-Type",
"multipart/form-data;boundary="+strBoundary);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setRequestProperty("Connection", "Keep-Alive");
conn.connect();
os = new BufferedOutputStream(conn.getOutputStream());
os.write(("--" + strBoundary +endLine).getBytes());
os.write((encodePostBody(params, strBoundary)).getBytes());
os.write((endLine + "--" + strBoundary + endLine).getBytes());
if (!dataparams.isEmpty()) {
for (String key: dataparams.keySet()){
os.write(("Content-Disposition: form-data; filename=\"" + key + "\"" + endLine).getBytes());
os.write(("Content-Type: content/unknown" + endLine + endLine).getBytes());
os.write(dataparams.getByteArray(key));
os.write((endLine + "--" + strBoundary + endLine).getBytes());
}
}
os.flush();
}
String response = "";
try {
response = read(conn.getInputStream());
} catch (FileNotFoundException e) {
// Error Stream contains JSON that we can parse to a FB error
response = read(conn.getErrorStream());
}
return response;
}
private static String read(InputStream in) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader r = new BufferedReader(new InputStreamReader(in), 1000);
for (String line = r.readLine(); line != null; line = r.readLine()) {
sb.append(line);
}
in.close();
return sb.toString();
}
public static void clearCookies(Context context) {
// Edge case: an illegal state exception is thrown if an instance of
// CookieSyncManager has not be created. CookieSyncManager is normally
// created by a WebKit view, but this might happen if you start the
// app, restore saved state, and click logout before running a UI
// dialog in a WebView -- in which case the app crashes
@SuppressWarnings("unused")
CookieSyncManager cookieSyncMngr =
CookieSyncManager.createInstance(context);
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
}
/**
* Parse a server response into a JSON Object. This is a basic
* implementation using org.json.JSONObject representation. More
* sophisticated applications may wish to do their own parsing.
*
* The parsed JSON is checked for a variety of error fields and
* a FacebookException is thrown if an error condition is set,
* populated with the error message and error type or code if
* available.
*
* @param response - string representation of the response
* @return the response as a JSON Object
* @throws JSONException - if the response is not valid JSON
* @throws FacebookError - if an error condition is set
*/
public static JSONObject parseJson(String response)
throws JSONException, FacebookError {
// Edge case: when sending a POST request to /[post_id]/likes
// the return value is 'true' or 'false'. Unfortunately
// these values cause the JSONObject constructor to throw
// an exception.
if (response.equals("false")) {
throw new FacebookError("request failed");
}
if (response.equals("true")) {
response = "{value : true}";
}
JSONObject json = new JSONObject(response);
// errors set by the server are not consistent
// they depend on the method and endpoint
if (json.has("error")) {
JSONObject error = json.getJSONObject("error");
throw new FacebookError(
error.getString("message"), error.getString("type"), 0);
}
if (json.has("error_code") && json.has("error_msg")) {
throw new FacebookError(json.getString("error_msg"), "",
Integer.parseInt(json.getString("error_code")));
}
if (json.has("error_code")) {
throw new FacebookError("request failed", "",
Integer.parseInt(json.getString("error_code")));
}
if (json.has("error_msg")) {
throw new FacebookError(json.getString("error_msg"));
}
if (json.has("error_reason")) {
throw new FacebookError(json.getString("error_reason"));
}
return json;
}
/**
* Display a simple alert dialog with the given text and title.
*
* @param context
* Android context in which the dialog should be displayed
* @param title
* Alert dialog title
* @param text
* Alert dialog message
*/
public static void showAlert(Context context, String title, String text) {
Builder alertBuilder = new Builder(context);
alertBuilder.setTitle(title);
alertBuilder.setMessage(text);
alertBuilder.create().show();
}
}

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.android.tests"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".Tests"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-sdk android:minSdkVersion="3" />
</manifest>

@ -0,0 +1,12 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
android.library.reference.1=../facebook/
# Project target.
target=android-3

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:background="@drawable/black"
android:gravity="center_horizontal">
<TextView android:id="@+id/publicTests"
android:text="@string/hello"
android:textColor="@drawable/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView android:id="@+id/publicErrors"
android:text="@string/hello"
android:textColor="@drawable/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button android:id="@+id/login"
android:text="@string/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="20dp"
android:paddingLeft="20dp"
android:layout_margin="20dp"
/>
<TextView android:id="@+id/authenticatedTests"
android:text="@string/hello"
android:textColor="@drawable/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView android:id="@+id/authenticatedErrors"
android:text="@string/hello"
android:textColor="@drawable/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button android:id="@+id/post"
android:text="@string/post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="20dp"
android:paddingLeft="20dp"
android:layout_margin="20dp"
/>
<TextView android:id="@+id/wallPost"
android:text="@string/hello"
android:textColor="@drawable/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView android:id="@+id/deletedPost"
android:text="@string/hello"
android:textColor="@drawable/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button android:id="@+id/logout"
android:text="@string/logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="20dp"
android:paddingLeft="20dp"
android:layout_margin="20dp"
/>
<TextView android:id="@+id/logoutTest"
android:text="@string/hello"
android:textColor="@drawable/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<drawable name="white">#ffffff</drawable>
<drawable name="black">#000000</drawable>
</resources>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hello">Hello World, Tests!</string>
<string name="login">Test Login</string>
<string name="post">Test UI Server</string>
<string name="logout">Test Logout</string>
<string name="app_name">Functional Tests</string>
</resources>

@ -0,0 +1,579 @@
package com.facebook.android.tests;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import org.json.JSONObject;
import com.facebook.android.AsyncFacebookRunner;
import com.facebook.android.DialogError;
import com.facebook.android.Facebook;
import com.facebook.android.FacebookError;
import com.facebook.android.Util;
import com.facebook.android.AsyncFacebookRunner.RequestListener;
import com.facebook.android.Facebook.DialogListener;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class Tests extends Activity {
// Your Facebook Application ID must be set before running this example
// See http://www.facebook.com/developers/createapp.php
public static final String APP_ID = "110862205611506";
private static final String[] PERMISSIONS =
new String[] {"publish_stream", "read_stream", "offline_access"};
TextView publicTestsText;
TextView publicErrorsText;
Button loginButton;
TextView authenticatedTestsText;
TextView authenticatedErrorsText;
Button postButton;
TextView wallPostText;
TextView deletedPostText;
Button logoutButton;
TextView logoutText;
Facebook authenticatedFacebook = new Facebook(APP_ID);
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
publicTestsText = (TextView) findViewById(R.id.publicTests);
publicErrorsText = (TextView) findViewById(R.id.publicErrors);
loginButton = (Button) findViewById(R.id.login);
authenticatedTestsText = (TextView) findViewById(
R.id.authenticatedTests);
authenticatedErrorsText = (TextView) findViewById(
R.id.authenticatedErrors);
postButton = (Button) findViewById(R.id.post);
wallPostText = (TextView) findViewById(R.id.wallPost);
deletedPostText = (TextView) findViewById(R.id.deletedPost);
logoutButton = (Button) findViewById(R.id.logout);
logoutText = (TextView) findViewById(R.id.logoutTest);
// button to test UI Server login method
loginButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
authenticatedFacebook.authorize(Tests.this, PERMISSIONS,
new TestLoginListener());
}
});
// button for testing UI server publish stream dialog
postButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
authenticatedFacebook.dialog(Tests.this, "stream.publish",
new TestUiServerListener());
}
});
// enable logout test button
logoutButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
runTestLogout();
}
});
runTestPublicApi();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
authenticatedFacebook.authorizeCallback(requestCode, resultCode, data);
}
public void runTestPublicApi() {
if (testPublicApi()) {
publicTestsText.setText("Public API tests passed");
publicTestsText.setTextColor(Color.GREEN);
} else {
publicTestsText.setText("Public API tests failed");
publicTestsText.setTextColor(Color.RED);
}
if (testPublicErrors()) {
publicErrorsText.setText("Public API errors passed");
publicErrorsText.setTextColor(Color.GREEN);
} else {
publicErrorsText.setText("Public API errors failed");
publicErrorsText.setTextColor(Color.RED);
}
}
public boolean testPublicApi() {
Facebook fb = new Facebook(APP_ID);
try {
Log.d("Tests", "Testing standard API call");
JSONObject response = Util.parseJson(fb.request("4"));
if (!response.getString("name").equals("Mark Zuckerberg")) {
return false;
}
Log.d("Tests", "Testing an API call with a specific method");
response = Util.parseJson(
fb.request("soneff", new Bundle(), "GET"));
if (!response.getString("name").equals("Steven Soneff")) {
return false;
}
Log.d("Tests", "Testing a public search query");
Bundle params = new Bundle();
params.putString("q", "facebook");
response = Util.parseJson(fb.request("search", params));
if (response.getJSONArray("data").length() == 0) return false;
Log.d("Tests", "Public API Tests passed");
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
public boolean testPublicErrors() {
Facebook fb = new Facebook(APP_ID);
try {
Bundle params = new Bundle();
Log.d("Tests", "Testing illegal post");
params.putString("message", "Hello World");
try {
Util.parseJson(fb.request("4", params, "POST"));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals("Unsupported post request.")) {
return false;
}
}
Log.d("Tests", "Testing illegal delete");
try {
Util.parseJson(fb.request("4", new Bundle(), "DELETE"));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals(
"An access token is required to request this " +
"resource.")) {
return false;
}
}
Log.d("Tests", "Testing illegal post to Zuck's feed");
try {
Util.parseJson(fb.request("4/feed", new Bundle(), "POST"));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals("(#200) The user hasn't " +
"authorized the application to perform this action")) {
return false;
}
}
Log.d("Tests", "Testing invalidly specified parameters");
try {
Util.parseJson(fb.request("bgolub?fields=id,name,picture"));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().startsWith("Unknown fields: picture?")) {
return false;
}
}
Log.d("Tests", "Testing request for 'me' is rejected without " +
"access_token");
try {
Util.parseJson(fb.request("me"));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals(
"An active access token must be used to " +
"query information about the current user.")) {
return false;
}
}
Log.d("Tests", "Testing empty request");
try {
Util.parseJson(fb.request(""));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals("Unsupported get request.")) {
return false;
}
}
Log.d("Tests", "Testing an invalid path");
try {
Util.parseJson(fb.request("invalidinvalidinvalidinvalid"));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals(
"(#803) Some of the aliases you requested do not " +
"exist: invalidinvalidinvalidinvalid")) {
return false;
}
}
Log.d("Tests", "Testing missing query parameter");
try {
Util.parseJson(fb.request("search", new Bundle(), "GET"));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals("No node specified")) return false;
}
Log.d("Tests", "Testing that API method is specified");
try {
fb.request(new Bundle());
return false;
} catch (IllegalArgumentException e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals(
"API method must be specified. " +
"(parameters must contain key \"method\" " +
"and value). See http://developers.facebook." +
"com/docs/reference/rest/")) {
return false;
}
}
Log.d("Tests", "Testing that old API request cannot be made " +
"without access token");
params.putString("method", "stream.publish");
try {
Util.parseJson(fb.request(params));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (e.getErrorCode() != 101 ||
!e.getMessage().equals("Invalid API key") ) {
return false;
}
}
Log.d("Tests", "Testing invalid access token");
try {
fb.setAccessToken("invalid");
Util.parseJson(fb.request("me", new Bundle(), "GET"));
return false;
} catch (FacebookError e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals("Invalid OAuth access token.")) {
return false;
}
}
Log.d("Tests", "Public API Error Tests passed");
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
public class TestLoginListener implements DialogListener {
public void onComplete(Bundle values) {
if (testAuthenticatedApi()) {
authenticatedTestsText.setText(
"Authenticated API tests passed");
authenticatedTestsText.setTextColor(Color.GREEN);
} else {
authenticatedTestsText.setText(
"Authenticated API tests failed");
authenticatedTestsText.setTextColor(Color.RED);
}
if (testAuthenticatedErrors()) {
authenticatedErrorsText.setText(
"Authenticated API errors passed");
authenticatedErrorsText.setTextColor(Color.GREEN);
} else {
authenticatedErrorsText.setText(
"Authenticated API errors failed");
authenticatedErrorsText.setTextColor(Color.RED);
}
}
public void onCancel() {
}
public void onError(DialogError e) {
e.printStackTrace();
}
public void onFacebookError(FacebookError e) {
e.printStackTrace();
}
}
public boolean testAuthenticatedApi() {
if (!authenticatedFacebook.isSessionValid()) return false;
try {
Log.d("Tests", "Testing request for 'me'");
String response = authenticatedFacebook.request("me");
JSONObject obj = Util.parseJson(response);
if (obj.getString("name") == null ||
obj.getString("name").equals("")) {
return false;
}
Log.d("Tests", "Testing graph API wall post");
Bundle parameters = new Bundle();
parameters.putString("message", "hello world");
parameters.putString("description", "test test test");
response = authenticatedFacebook.request("me/feed", parameters,
"POST");
Log.d("Tests", "got response: " + response);
if (response == null || response.equals("") ||
response.equals("false")) {
return false;
}
Log.d("Tests", "Testing graph API delete");
response = response.replaceAll("\\{\"id\":\"", "");
response = response.replaceAll("\"\\}", "");
response = authenticatedFacebook.request(response, new Bundle(),
"DELETE");
if (!response.equals("true")) return false;
Log.d("Tests", "Testing old API wall post");
parameters = new Bundle();
parameters.putString("method", "stream.publish");
parameters.putString("attachment",
"{\"name\":\"Name=Title\"," +
"\"href\":\"http://www.google.fr/\",\"" +
"caption\":\"Caption\",\"description\":\"Description" +
"\",\"media\":[{\"type\":\"image\",\"src\":" +
"\"http://www.kratiroff.com/logo-facebook.jpg\"," +
"\"href\":\"http://developers.facebook.com/\"}]," +
"\"properties\":{\"another link\":{\"text\":\"" +
"Facebook homepage\",\"href\":\"http://www.facebook." +
"com\"}}}");;
response = authenticatedFacebook.request(parameters);
Log.d("Tests", "got response: " + response);
if (response == null || response.equals("") ||
response.equals("false")) {
return false;
}
Log.d("Tests", "Testing wall post delete");
response = response.replaceAll("\"", "");
response = authenticatedFacebook.request(
response, new Bundle(), "DELETE");
if (!response.equals("true")) return false;
Log.d("Tests", "All Authenticated Tests Passed");
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
public boolean testAuthenticatedErrors() {
if (!authenticatedFacebook.isSessionValid()) return false;
Log.d("Tests", "Testing that request for 'me/invalid' is rejected");
try {
Util.parseJson(authenticatedFacebook.request("me/invalid"));
return false;
} catch (Throwable e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals("Unknown path components: /invalid")) {
return false;
}
}
Log.d("Tests", "Testing that old API call with invalid method fails");
Bundle params = new Bundle();
params.putString("method", "something_invalid");
try {
Util.parseJson(authenticatedFacebook.request(params));
return false;
} catch (Throwable e) {
Log.d("Tests", "*" + e.getMessage() + "*");
if (!e.getMessage().equals("Unknown method") ) {
return false;
}
}
Log.d("Tests", "All Authenticated Error Tests Passed");
return true;
}
public class TestUiServerListener implements DialogListener {
public void onComplete(Bundle values) {
final String postId = values.getString("post_id");
if (postId != null) {
Log.d("Facebook-Example", "Dialog Success! post_id=" + postId);
new AsyncFacebookRunner(authenticatedFacebook).request(postId,
new TestPostRequestListener());
} else {
Tests.this.runOnUiThread(new Runnable() {
public void run() {
wallPostText.setText("Wall Post Failure");
wallPostText.setTextColor(Color.RED);
}
});
}
}
public void onCancel() { }
public void onError(DialogError e) {
e.printStackTrace();
}
public void onFacebookError(FacebookError e) {
e.printStackTrace();
}
}
public class TestPostRequestListener implements RequestListener {
public void onComplete(final String response, final Object state) {
Log.d("Tests", "Got response: " + response);
try {
JSONObject json = Util.parseJson(response);
//final String message = json.getString("message");
String postId = json.getString("id");
Tests.this.runOnUiThread(new Runnable() {
public void run() {
wallPostText.setText("Wall Post Success");
wallPostText.setTextColor(Color.GREEN);
}
});
Log.d("Tests", "Testing wall post delete");
if (testPostDelete(postId)) {
Tests.this.runOnUiThread(new Runnable() {
public void run() {
deletedPostText.setText("Deleted Post Success");
deletedPostText.setTextColor(Color.GREEN);
}
});
} else {
Tests.this.runOnUiThread(new Runnable() {
public void run() {
deletedPostText.setText("Deleted Post Failure");
deletedPostText.setTextColor(Color.RED);
}
});
}
} catch (Throwable e) {
e.printStackTrace();
Tests.this.runOnUiThread(new Runnable() {
public void run() {
wallPostText.setText("Wall Post Failure");
wallPostText.setTextColor(Color.RED);
}
});
}
}
public void onFacebookError(FacebookError e, final Object state) {
e.printStackTrace();
}
public void onFileNotFoundException(FileNotFoundException e,
final Object state) {
e.printStackTrace();
}
public void onIOException(IOException e, final Object state) {
e.printStackTrace();
}
public void onMalformedURLException(MalformedURLException e,
final Object state) {
e.printStackTrace();
}
}
public boolean testPostDelete(String postId) {
try {
String deleteResponse =
authenticatedFacebook.request(postId, new Bundle(), "DELETE");
return deleteResponse.equals("true");
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
public void runTestLogout() {
if (testLogout()) {
logoutText.setText("Logout Tests Passed");
logoutText.setTextColor(Color.GREEN);
} else {
logoutText.setText("Logout Tests Failed");
logoutText.setTextColor(Color.RED);
}
}
public boolean testLogout() {
try {
Log.d("Tests", "Testing logout");
String response = authenticatedFacebook.logout(this);
Log.d("Tests", "Got logout response: *" + response + "*");
if (!response.equals("true")) {
return false;
}
Log.d("Tests", "Testing logout on logged out facebook session");
try {
Util.parseJson(authenticatedFacebook.logout(this));
return false;
} catch (FacebookError e) {
if (e.getErrorCode() != 101 ||
!e.getMessage().equals("Invalid API key") ) {
return false;
}
}
Log.d("Tests", "Testing logout on unauthenticated object");
try {
Util.parseJson(new Facebook(APP_ID).logout(this));
return false;
} catch (FacebookError e) {
if (e.getErrorCode() != 101 ||
!e.getMessage().equals("Invalid API key") ) {
return false;
}
}
Log.d("Tests", "All Logout Tests Passed");
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
// test bad UI server method?
// test invalid permission? <-- UI server test
}
Loading…
Cancel
Save