diff --git a/FacebookLib/.classpath b/FacebookLib/.classpath new file mode 100644 index 0000000..609aa00 --- /dev/null +++ b/FacebookLib/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/FacebookLib/.project b/FacebookLib/.project new file mode 100644 index 0000000..194d95d --- /dev/null +++ b/FacebookLib/.project @@ -0,0 +1,33 @@ + + + FacebookLib + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/FacebookLib/AndroidManifest.xml b/FacebookLib/AndroidManifest.xml new file mode 100644 index 0000000..e6940f8 --- /dev/null +++ b/FacebookLib/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/FacebookLib/default.properties b/FacebookLib/default.properties new file mode 100644 index 0000000..3ce0e8a --- /dev/null +++ b/FacebookLib/default.properties @@ -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=Google Inc.:Google APIs:9 diff --git a/FacebookLib/gen/com/facebook/android/R.java b/FacebookLib/gen/com/facebook/android/R.java new file mode 100644 index 0000000..85c149c --- /dev/null +++ b/FacebookLib/gen/com/facebook/android/R.java @@ -0,0 +1,16 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package com.facebook.android; + +public final class R { + public static final class attr { + } + public static final class drawable { + public static final int facebook_icon=0x7f020000; + } +} diff --git a/FacebookLib/res/drawable-hdpi/facebook_icon.png b/FacebookLib/res/drawable-hdpi/facebook_icon.png new file mode 100644 index 0000000..af8e077 Binary files /dev/null and b/FacebookLib/res/drawable-hdpi/facebook_icon.png differ diff --git a/FacebookLib/res/drawable-ldpi/facebook_icon.png b/FacebookLib/res/drawable-ldpi/facebook_icon.png new file mode 100644 index 0000000..5bbc2cc Binary files /dev/null and b/FacebookLib/res/drawable-ldpi/facebook_icon.png differ diff --git a/FacebookLib/res/drawable/facebook_icon.png b/FacebookLib/res/drawable/facebook_icon.png new file mode 100644 index 0000000..413396b Binary files /dev/null and b/FacebookLib/res/drawable/facebook_icon.png differ diff --git a/FacebookLib/src/com/facebook/android/AsyncFacebookRunner.java b/FacebookLib/src/com/facebook/android/AsyncFacebookRunner.java new file mode 100644 index 0000000..be3870a --- /dev/null +++ b/FacebookLib/src/com/facebook/android/AsyncFacebookRunner.java @@ -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: + * + * Bundle parameters = new Bundle(); + * parameters.putString("method", "auth.expireSession", new Listener()); + * String response = request(parameters); + * + * + * @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); + + } + +} diff --git a/FacebookLib/src/com/facebook/android/DialogError.java b/FacebookLib/src/com/facebook/android/DialogError.java new file mode 100644 index 0000000..51d06c9 --- /dev/null +++ b/FacebookLib/src/com/facebook/android/DialogError.java @@ -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; + } + +} diff --git a/FacebookLib/src/com/facebook/android/Facebook.java b/FacebookLib/src/com/facebook/android/Facebook.java new file mode 100644 index 0000000..a2ab8b4 --- /dev/null +++ b/FacebookLib/src/com/facebook/android/Facebook.java @@ -0,0 +1,757 @@ +/* + * 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.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +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(Context context, final DialogListener listener) { + authorize(context, new String[] {}, DEFAULT_AUTH_ACTIVITY_CODE, + listener); + } + + /** + * Authorize method that grants custom permissions. + * + * See authorize() below for @params. + */ + public void authorize(Context context, String[] permissions, + final DialogListener listener) { + authorize(context, 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(Context context, 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(context, 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 context + * 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(Context context, String[] permissions) { + Bundle params = new Bundle(); + if (permissions.length > 0) { + params.putString("scope", TextUtils.join(",", permissions)); + } + CookieSyncManager.createInstance(context); + dialog(context, LOGIN, params, new DialogListener() { + + public void onComplete(Bundle values, Dialog dialog) { + // 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, dialog); + } 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(), null); + } 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: + * + * Bundle parameters = new Bundle(); + * parameters.putString("method", "auth.expireSession"); + * String response = request(parameters); + * + * + * @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 FbDialog 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 { + FbDialog fbDialog = new FbDialog(context, url, listener); + fbDialog.show(); + return fbDialog; + } + + return null; + } + + /** + * @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, Dialog dialog); + + /** + * 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"; +// +} diff --git a/FacebookLib/src/com/facebook/android/FacebookError.java b/FacebookLib/src/com/facebook/android/FacebookError.java new file mode 100644 index 0000000..16836db --- /dev/null +++ b/FacebookLib/src/com/facebook/android/FacebookError.java @@ -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; + } + +} diff --git a/FacebookLib/src/com/facebook/android/FbDialog.java b/FacebookLib/src/com/facebook/android/FbDialog.java new file mode 100644 index 0000000..db6c377 --- /dev/null +++ b/FacebookLib/src/com/facebook/android/FbDialog.java @@ -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, FbDialog.this); + } 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(); + } + + } +} diff --git a/FacebookLib/src/com/facebook/android/Util.java b/FacebookLib/src/com/facebook/android/Util.java new file mode 100644 index 0000000..cf5e97b --- /dev/null +++ b/FacebookLib/src/com/facebook/android/Util.java @@ -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(); + } + +}