diff --git a/Android/Facebook/README.md b/Android/Facebook/README.md new file mode 100644 index 00000000..2602fc14 --- /dev/null +++ b/Android/Facebook/README.md @@ -0,0 +1,36 @@ +# Facebook for PhoneGap on Android # +by Jos Shepherd + +This is an attempt to make a PhoneGap plugin from the Facebook Android SDK: +https://github.com/facebook/facebook-android-sdk + +It is currently not functional (the login dialog is silently failing to appear) + + +## Licence ## + +The MIT License + +Copyright (c) 2010 Jos Shepherd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + + diff --git a/Android/Facebook/src/com/facebook/android/AsyncFacebookRunner.java b/Android/Facebook/src/com/facebook/android/AsyncFacebookRunner.java new file mode 100644 index 00000000..d5d07efa --- /dev/null +++ b/Android/Facebook/src/com/facebook/android/AsyncFacebookRunner.java @@ -0,0 +1,268 @@ +/* + * 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 ssoneff@facebook.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. + */ + public void logout(final Context context, final RequestListener listener) { + 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")); + return; + } + listener.onComplete(response); + } catch (FileNotFoundException e) { + listener.onFileNotFoundException(e); + } catch (MalformedURLException e) { + listener.onMalformedURLException(e); + } catch (IOException e) { + listener.onIOException(e); + } + } + }.start(); + } + + /** + * 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. + */ + public void request(Bundle parameters, + RequestListener listener) { + request(null, parameters, "GET", listener); + } + + /** + * 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. + */ + public void request(String graphPath, + RequestListener listener) { + request(graphPath, new Bundle(), "GET", listener); + } + + /** + * 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. + */ + public void request(String graphPath, + Bundle parameters, + RequestListener listener) { + request(graphPath, parameters, "GET", listener); + } + + /** + * 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. + */ + public void request(final String graphPath, + final Bundle parameters, + final String httpMethod, + final RequestListener listener) { + new Thread() { + @Override public void run() { + try { + String resp = fb.request(graphPath, parameters, httpMethod); + listener.onComplete(resp); + } catch (FileNotFoundException e) { + listener.onFileNotFoundException(e); + } catch (MalformedURLException e) { + listener.onMalformedURLException(e); + } catch (IOException e) { + listener.onIOException(e); + } + } + }.start(); + } + + + /** + * Callback interface for API requests. + * + */ + 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + /** + * 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); + + } + +} diff --git a/Android/Facebook/src/com/facebook/android/DialogError.java b/Android/Facebook/src/com/facebook/android/DialogError.java new file mode 100644 index 00000000..ff50a940 --- /dev/null +++ b/Android/Facebook/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/Android/Facebook/src/com/facebook/android/Facebook.java b/Android/Facebook/src/com/facebook/android/Facebook.java new file mode 100644 index 00000000..20ac15fd --- /dev/null +++ b/Android/Facebook/src/com/facebook/android/Facebook.java @@ -0,0 +1,756 @@ +/* + * 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 Steven Soneff (ssoneff@facebook.com), Jim Brusstar + * (jimbru@facebook.com), Yariv Sadan (yariv@facebook.com), + * Luke Shepard (lshepard@facebook.com) + */ +public class Facebook { + + /* Strings used in the OAuth 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 = "login"; + + // 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 OAUTH_ENDPOINT = + "https://www.facebook.com/dialog/oauth"; + protected static String UI_SERVER = + "https://www.facebook.com/connect/uiserver.php"; + 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; + + public Facebook(String applicationId) { + if (applicationId == null) { + throw new IllegalArgumentException( + "You must specify your application ID when instantiating a Facebook " + + "object. See README for details."); + } + mAppId = applicationId; + } + + /** + * Default authorize method doesn't require any permissions. + */ + public void authorize(Activity activity, final DialogListener listener) { + authorize(activity, new String[] {}, DEFAULT_AUTH_ACTIVITY_CODE, + listener); + } + + /** + * Overloaded signature. See authorize() below. + */ + public void authorize(Activity activity, String[] permissions, + final DialogListener listener) { + authorize(activity, permissions, DEFAULT_AUTH_ACTIVITY_CODE, listener); + } + + /** + * 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, mAppId, 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 applicationId, + String[] permissions) { + Bundle params = new Bundle(); + params.putString("client_id", applicationId); + 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, mAppId, + 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: + * 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 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. "GET", "POST", "DELETE" + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the response + */ + public String request(String graphPath, Bundle parameters, String httpMethod) + throws FileNotFoundException, MalformedURLException, IOException { + parameters.putString("format", "json"); + if (isSessionValid()) { + parameters.putString(TOKEN, getAccessToken()); + } + String url = graphPath != null ? GRAPH_BASE_URL + graphPath + : RESTSERVER_URL; + return Util.openUrl(url, httpMethod, parameters); + } + + /** + * 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. "login", + * "stream.publish", ... + * @param parameters + * key-value string 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; + if (action.equals(LOGIN)) { + endpoint = OAUTH_ENDPOINT; + parameters.putString("type", "user_agent"); + parameters.putString("redirect_uri", REDIRECT_URI); + } else { + endpoint = UI_SERVER; + parameters.putString("method", action); + parameters.putString("next", REDIRECT_URI); + } + parameters.putString("display", "touch"); + 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 { + System.out.println("FbDialog url: "+url); + + FbDialog test = new FbDialog(context, url, listener); + System.out.println("Constructed"); + + test.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"; + +} diff --git a/Android/Facebook/src/com/facebook/android/FacebookError.java b/Android/Facebook/src/com/facebook/android/FacebookError.java new file mode 100644 index 00000000..c98bed94 --- /dev/null +++ b/Android/Facebook/src/com/facebook/android/FacebookError.java @@ -0,0 +1,49 @@ +/* + * 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/Android/Facebook/src/com/facebook/android/FbDialog.java b/Android/Facebook/src/com/facebook/android/FbDialog.java new file mode 100644 index 00000000..908b10f5 --- /dev/null +++ b/Android/Facebook/src/com/facebook/android/FbDialog.java @@ -0,0 +1,185 @@ +/* + * 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); + Log.d("FbDialog","construct"); + + mUrl = url; + mListener = listener; + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d("FbDialog","onCreate"); + + 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(); + } + + } +} diff --git a/Android/Facebook/src/com/facebook/android/Util.java b/Android/Facebook/src/com/facebook/android/Util.java new file mode 100644 index 00000000..bb90a1d6 --- /dev/null +++ b/Android/Facebook/src/com/facebook/android/Util.java @@ -0,0 +1,299 @@ +/* + * 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(); + } + +} diff --git a/Android/Facebook/src/com/phonegap/plugins/facebook/FacebookAuth.java b/Android/Facebook/src/com/phonegap/plugins/facebook/FacebookAuth.java new file mode 100755 index 00000000..50b847ad --- /dev/null +++ b/Android/Facebook/src/com/phonegap/plugins/facebook/FacebookAuth.java @@ -0,0 +1,118 @@ +/* + * PhoneGap is available under *either* the terms of the modified BSD license *or* the + * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text. + * + * Copyright (c) 2005-2010, Nitobi Software Inc. + * Copyright (c) 2010, IBM Corporation + */ +package com.phonegap.plugins.facebook; + +import org.json.JSONArray; +import org.json.JSONException; + +import android.content.Intent; +import android.content.Context; +import android.app.Activity; + +import android.net.Uri; +import android.os.Bundle; + +import com.phonegap.api.Plugin; +import com.phonegap.api.PluginResult; + +import com.facebook.android.Facebook; +import com.facebook.android.AsyncFacebookRunner; +import com.facebook.android.Facebook.DialogListener; +import com.facebook.android.DialogError; +import com.facebook.android.FacebookError; + +public class FacebookAuth extends Plugin { + + public static final String APP_ID = "175729095772478"; + private Facebook mFb; + + /** + * Executes the request and returns PluginResult. + * + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackId The callback id used when calling back into JavaScript. + * @return A PluginResult object with a status and message. + */ + public PluginResult execute(String action, JSONArray args, String callbackId) { + PluginResult.Status status = PluginResult.Status.OK; + String result = ""; + System.out.println("execute: "+ action); + + try { + if (action.equals("authorize")) { + + result = this.authorize(args.getString(0)); + if (result.length() > 0) { + status = PluginResult.Status.ERROR; + } + System.out.println("result: "+ result); + + } + + return new PluginResult(status, result); + } catch (JSONException e) { + System.out.println("exception: "+ action); + + return new PluginResult(PluginResult.Status.JSON_EXCEPTION); + } + } + + /** + * Identifies if action to be executed returns a value and should be run synchronously. + * + * @param action The action to execute + * @return T=returns value + */ + public boolean isSynch(String action) { + return false; + } + + /** + * Called by AccelBroker when listener is to be shut down. + * Stop listener. + */ + public void onDestroy() { + } + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + /** + * Display a new browser with the specified URL. + * + * @param url The url to load. + * @param usePhoneGap Load url in PhoneGap webview + * @return "" if ok, or error message. + */ + public String authorize(String url) { + + this.mFb = new Facebook(APP_ID); + this.mFb.authorize((Activity) this.ctx, new AuthorizeListener()); + return "string"; + } + + class AuthorizeListener implements DialogListener { + public void onComplete(Bundle values) { + // Handle a successful login + } + public void onFacebookError(FacebookError e) { + e.printStackTrace(); + } + + public void onError(DialogError e) { + e.printStackTrace(); + } + + public void onCancel() { + } + } + + +} diff --git a/Android/Facebook/www/facebook.js b/Android/Facebook/www/facebook.js new file mode 100755 index 00000000..426ec4ea --- /dev/null +++ b/Android/Facebook/www/facebook.js @@ -0,0 +1,34 @@ +/* + * PhoneGap is available under *either* the terms of the modified BSD license *or* the + * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text. + * + * Copyright (c) 2005-2010, Nitobi Software Inc. + * Copyright (c) 2010, IBM Corporation + */ + + +(function(){ + /** + * Constructor + */ + function Facebook() {} + + /** + * Display a new browser with the specified URL. + * + * @param url The url to load + * @param usePhoneGap Load url in PhoneGap webview [optional] + */ + Facebook.prototype.authorize = function(url) { + PhoneGap.exec(null, null, "FacebookAuth", "authorize", [url]); + }; + + /** + * Load ChildBrowser + */ + PhoneGap.addConstructor(function() { + PhoneGap.addPlugin("facebook", new Facebook()); + PluginManager.addService("FacebookAuth", "com.phonegap.plugins.facebook.FacebookAuth"); + }); + +})(); \ No newline at end of file