/*
* 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.content.Context;
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 Steven Soneff (ssoneff@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";
private static final String LOGIN = "login";
/* Facebook server endpoints: may be modified in a subclass for testing */
protected static String OAUTH_ENDPOINT =
"https://graph.facebook.com/oauth/authorize";
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;
/**
* Starts a dialog which prompts the user to log in to Facebook and grant
* the requested permissions to the given application.
*
* This method implements the OAuth 2.0 User-Agent flow to retrieve an
* access token for use in API requests. 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.
*
* @param context
* The Android context in which we want to display the
* authorization dialog
* @param applicationId
* The Facebook application identifier e.g. "350685531728"
* @param permissions
* A list of permission 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 listener
* Callback interface for notifying the calling application when
* the dialog has completed, failed, or been canceled.
*/
public void authorize(Context context,
String applicationId,
String[] permissions,
final DialogListener listener) {
Bundle params = new Bundle();
params.putString("client_id", applicationId);
if (permissions.length > 0) {
params.putString("scope", TextUtils.join(",", permissions));
}
CookieSyncManager.createInstance(context);
dialog(context, 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());
listener.onComplete(values);
} else {
onFacebookError(new FacebookError(
"failed to receive access_token"));
}
}
public void onError(DialogError error) {
Log.d("Facebook-authorize", "Login failed: " + error);
listener.onError(error);
}
public void onFacebookError(FacebookError error) {
Log.d("Facebook-authorize", "Login failed: " + error);
listener.onFacebookError(error);
}
public void onCancel() {
Log.d("Facebook-authorize", "Login cancelled");
listener.onCancel();
}
});
}
/**
* Invalidate the current user session by removing the access token in
* memory, clearing the browser cookie, and calling auth.expireSession
* through the API.
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* @param context
* The Android context in which the logout should be called: it
* should be the same context in which the login occurred in
* order to clear any stored cookies
* @throws IOException
* @throws MalformedURLException
* @return JSON string representation of the auth.expireSession response
* ("true" if successful)
*/
public String logout(Context context)
throws MalformedURLException, IOException {
Util.clearCookies(context);
Bundle b = new Bundle();
b.putString("method", "auth.expireSession");
String response = request(b);
setAccessToken(null);
setAccessExpires(0);
return response;
}
/**
* Make a request to Facebook's old (pre-graph) API with the given
* parameters. One of the parameter keys must be "method" and its value
* should be a valid REST server API method.
*
* See http://developers.facebook.com/docs/reference/rest/
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* Example:
* <code>
* Bundle parameters = new Bundle();
* parameters.putString("method", "auth.expireSession");
* String response = request(parameters);
* </code>
*
* @param parameters
* Key-value pairs of parameters to the request. Refer to the
* documentation: one of the parameters must be "method".
* @throws IOException
* if a network error occurs
* @throws MalformedURLException
* if accessing an invalid endpoint
* @throws IllegalArgumentException
* if one of the parameters is not "method"
* @return JSON string representation of the response
*/
public String request(Bundle parameters)
throws MalformedURLException, IOException {
if (!parameters.containsKey("method")) {
throw new IllegalArgumentException("API method must be specified. "
+ "(parameters must contain key \"method\" and value). See"
+ " http://developers.facebook.com/docs/reference/rest/");
}
return request(null, parameters, "GET");
}
/**
* Make a request to the Facebook Graph API without any parameters.
*
* See http://developers.facebook.com/docs/api
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @throws IOException
* @throws MalformedURLException
* @return JSON string representation of the response
*/
public String request(String graphPath)
throws MalformedURLException, IOException {
return request(graphPath, new Bundle(), "GET");
}
/**
* Make a request to the Facebook Graph API with the given string
* parameters using an HTTP GET (default method).
*
* See http://developers.facebook.com/docs/api
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @param parameters
* key-value string parameters, e.g. the path "search" with
* parameters "q" : "facebook" would produce a query for the
* following graph resource:
* https://graph.facebook.com/search?q=facebook
* @throws IOException
* @throws MalformedURLException
* @return JSON string representation of the response
*/
public String request(String graphPath, Bundle parameters)
throws MalformedURLException, IOException {
return request(graphPath, parameters, "GET");
}
/**
* Synchronously make a request to the Facebook Graph API with the given
* HTTP method and string parameters. Note that binary data parameters
* (e.g. pictures) are not yet supported by this helper function.
*
* See http://developers.facebook.com/docs/api
*
* Note that this method blocks waiting for a network response, so do not
* call it in a UI thread.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
* about the currently logged authenticated user, provide "me",
* which will fetch http://graph.facebook.com/me
* @param 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 {
new FbDialog(context, url, listener).show();
}
}
/**
* @return boolean - whether this object has an non-expired session token
*/
public boolean isSessionValid() {
return (getAccessToken() != null) && ((getAccessExpires() == 0) ||
(System.currentTimeMillis() < getAccessExpires()));
}
/**
* Retrieve the OAuth 2.0 access token for API access: treat with care.
* Returns null if no session exists.
*
* @return String - access token
*/
public String getAccessToken() {
return mAccessToken;
}
/**
* Retrieve the current session's expiration time (in milliseconds since
* Unix epoch), or 0 if the session doesn't expire or doesn't exist.
*
* @return long - session expiration time
*/
public long getAccessExpires() {
return mAccessExpires;
}
/**
* Set the OAuth 2.0 access token for API access.
*
* @param token - access token
*/
public void setAccessToken(String token) {
mAccessToken = token;
}
/**
* Set the current session's expiration time (in milliseconds since Unix
* epoch), or 0 if the session doesn't expire.
*
* @param time - timestamp in milliseconds
*/
public void setAccessExpires(long time) {
mAccessExpires = time;
}
/**
* Set the current session's duration (in seconds since Unix epoch).
*
* @param expiresIn - duration in seconds
*/
public void setAccessExpiresIn(String expiresIn) {
if (expiresIn != null && !expiresIn.equals("0")) {
setAccessExpires(System.currentTimeMillis()
+ Integer.parseInt(expiresIn) * 1000);
}
}
/**
* 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();
}
}