Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'master' of git://github.com/facebook/facebook-android-sdk

  • Loading branch information...
commit cc7447abcc3765c743c9fd012118b5b71940ef00 2 parents 44f4335 + c58af0b
@ksperling authored
View
4 README.md
@@ -19,4 +19,6 @@ This library includes three sample applications to guide you in development.
Report Issues/Bugs
===============
-[Facebook Stackoverflow](http://facebook.stackoverflow.com/questions/tagged/android)
+[Bugs](https://developers.facebook.com/bugs)
+
+[Questions](http://facebook.stackoverflow.com/questions/tagged/android)
View
67 examples/Hackbook/res/layout/token_refresh.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="@color/black">
+ <TextView
+ android:id="@+id/tokenLabel"
+ android:text="@string/access_token_label"
+ android:textColor="@color/white"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="3dp"
+ android:paddingLeft="3dp" />
+ <EditText
+ android:id="@+id/tokenEdit"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:editable="false" />
+ <TextView
+ android:id="@+id/tokenExpiresLabel"
+ android:text="@string/access_token_expires_label"
+ android:textColor="@color/white"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="3dp"
+ android:paddingLeft="3dp" />
+ <EditText
+ android:id="@+id/tokenExpiresEdit"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:editable="false" />
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="2dip"
+ android:background="@color/grey" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <Button
+ android:id="@+id/refresh_button"
+ android:text="@string/refresh_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp" />
+ </LinearLayout>
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="2dip"
+ android:background="@color/grey" />
+ <TextView
+ android:id="@+id/tip_label"
+ android:text="@string/tip_label"
+ android:textColor="@color/white"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="3dp"
+ android:paddingLeft="3dp" />
+ <TextView
+ android:id="@+id/usefulTip"
+ android:text="@string/refresh_token_tip"
+ android:textColor="@color/white"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="5dp"
+ android:paddingLeft="3dp" />
+</LinearLayout>
View
8 examples/Hackbook/res/values/strings.xml
@@ -19,6 +19,7 @@
<string name="target_url">Target URL:</string>
<string name="exception">Exception: </string>
<string name="tip_label">Tip:</string>
+ <string name="error">Error</string>
<string name="facebook_error">Facebook Error: </string>
<string name="api_response">Api Response:</string>
<string name="view_source">View Source</string>
@@ -77,4 +78,11 @@
<string name="enable_gps_title">Enable GPS</string>
<string name="enable_gps">Please enable GPS to get nearby places and check-in</string>
<string name="gps_settings">GPS Settings</string>
+ <string name="refresh_token_title">Refresh Token</string>
+ <string name="refresh_button">Refresh</string>
+ <string name="refresh_button_pending">Refreshing…</string>
+ <string name="refresh_token_tip">In most cases the access token should be refreshed silently when the application is running (for example see Hackbook onResume method).</string>
+ <string name="refresh_token_binding_error">Binding to the Facebook Android Application failed (is it installed?).</string>
+ <string name="access_token_label">Current access token:</string>
+ <string name="access_token_expires_label">Token expires at:</string>
</resources>
View
22 examples/Hackbook/src/com/facebook/android/Hackbook.java
@@ -66,7 +66,7 @@
private ListView list;
String[] main_items = { "Update Status", "App Requests", "Get Friends", "Upload Photo",
- "Place Check-in", "Run FQL Query", "Graph API Explorer" };
+ "Place Check-in", "Run FQL Query", "Graph API Explorer", "Token Refresh" };
String[] permissions = { "offline_access", "publish_stream", "user_photos", "publish_checkins",
"photo_upload" };
@@ -117,9 +117,13 @@ public void onCreate(Bundle savedInstanceState) {
@Override
public void onResume() {
super.onResume();
- if (Utility.mFacebook != null && !Utility.mFacebook.isSessionValid()) {
- mText.setText("You are logged out! ");
- mUserPic.setImageBitmap(null);
+ if(Utility.mFacebook != null) {
+ if (!Utility.mFacebook.isSessionValid()) {
+ mText.setText("You are logged out! ");
+ mUserPic.setImageBitmap(null);
+ } else {
+ Utility.mFacebook.extendAccessTokenIfNeeded(this, null);
+ }
}
}
@@ -210,7 +214,7 @@ public void onItemClick(AdapterView<?> arg0, View v, int position, long arg3) {
* Source Tag: friends_tag You can get friends using
* graph.facebook.com/me/friends, this returns the list sorted by
* UID OR using the friend table. With this you can sort the way you
- * want it.
+ * want it.
* Friend table - https://developers.facebook.com/docs/reference/fql/friend/
* User table - https://developers.facebook.com/docs/reference/fql/user/
*/
@@ -363,6 +367,14 @@ public void onClick(DialogInterface dialog, int which) {
startActivity(myIntent);
break;
}
+
+ case 7: {
+ if(!Utility.mFacebook.isSessionValid()) {
+ Util.showAlert(this, "Warning", "You must first log in.");
+ } else {
+ new TokenRefreshDialog(Hackbook.this).show();
+ }
+ }
}
}
View
96 examples/Hackbook/src/com/facebook/android/TokenRefreshDialog.java
@@ -0,0 +1,96 @@
+package com.facebook.android;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+public class TokenRefreshDialog extends Dialog {
+
+ private EditText tokenEdit, tokenExpiresEdit;
+ private TextView mUsefulTip;
+ private Button mRefreshButton;
+ private Activity activity;
+
+ public TokenRefreshDialog(Activity activity) {
+ super(activity);
+ this.activity = activity;
+ setTitle(R.string.refresh_token_title);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.token_refresh);
+
+ tokenEdit = (EditText) findViewById(R.id.tokenEdit);
+ tokenEdit.setText(Utility.mFacebook.getAccessToken());
+
+ tokenExpiresEdit = (EditText) findViewById(R.id.tokenExpiresEdit);
+ setExpiresAt(Utility.mFacebook.getAccessExpires());
+
+ mUsefulTip = (TextView) findViewById(R.id.usefulTip);
+ mUsefulTip.setMovementMethod(LinkMovementMethod.getInstance());
+ mRefreshButton = (Button) findViewById(R.id.refresh_button);
+
+ mRefreshButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ changeButtonState(false);
+ RefreshTokenListener listener = new RefreshTokenListener();
+ if (!Utility.mFacebook.extendAccessToken(activity, listener)) {
+ listener.onError(new Error(
+ activity.getString(R.string.refresh_token_binding_error)));
+ }
+ }
+ });
+ }
+
+ private class RefreshTokenListener implements Facebook.ServiceListener {
+
+ @Override
+ public void onFacebookError(FacebookError e) {
+ changeButtonState(true);
+ String title = String.format(activity.getString(R.string.facebook_error) + "%d",
+ e.getErrorCode());
+ Util.showAlert(activity, title, e.getMessage());
+ }
+
+ @Override
+ public void onError(Error e) {
+ changeButtonState(true);
+ Util.showAlert(activity, activity.getString(R.string.error), e.getMessage());
+ }
+
+ @Override
+ public void onComplete(Bundle values) {
+ changeButtonState(true);
+
+ // The access_token and expires_at values are automatically updated,
+ // so they can be obtained by using:
+ // - Facebook.getAccessToken()
+ // - Facebook.getAccessExpires()
+ // methods, but we can also get them from the 'values' bundle.
+ tokenEdit.setText(values.getString(Facebook.TOKEN));
+ setExpiresAt(values.getLong(Facebook.EXPIRES));
+ }
+ }
+
+ private void changeButtonState(boolean enabled) {
+ mRefreshButton.setEnabled(enabled);
+ mRefreshButton.setText(enabled ? R.string.refresh_button : R.string.refresh_button_pending);
+ }
+
+ private void setExpiresAt(long time) {
+ DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
+ tokenExpiresEdit.setText(dateFormat.format(new Date(time)));
+ }
+}
View
188 facebook/src/com/facebook/android/Facebook.java
@@ -25,14 +25,21 @@
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.ServiceConnection;
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.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.CookieSyncManager;
@@ -72,6 +79,7 @@
"https://api.facebook.com/restserver.php";
private String mAccessToken = null;
+ private long mLastAccessUpdate = 0;
private long mAccessExpires = 0;
private String mAppId;
@@ -79,6 +87,10 @@
private String[] mAuthPermissions;
private int mAuthActivityCode;
private DialogListener mAuthDialogListener;
+
+ // If the last time we extended the access token was more than 24 hours ago
+ // we try to refresh the access token again.
+ final private long REFRESH_TOKEN_BARRIER = 24L * 60L * 60L * 1000L;
/**
* Constructor for Facebook object.
@@ -247,16 +259,16 @@ private boolean startSingleSignOn(Activity activity, String applicationId,
* 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 context
* @param intent
* @param validSignature
* @return true if the app's signature matches the expected signature.
*/
- private boolean validateAppSignatureForIntent(Activity activity,
+ private boolean validateAppSignatureForIntent(Context context,
Intent intent) {
ResolveInfo resolveInfo =
- activity.getPackageManager().resolveActivity(intent, 0);
+ context.getPackageManager().resolveActivity(intent, 0);
if (resolveInfo == null) {
return false;
}
@@ -264,7 +276,7 @@ private boolean validateAppSignatureForIntent(Activity activity,
String packageName = resolveInfo.activityInfo.packageName;
PackageInfo packageInfo;
try {
- packageInfo = activity.getPackageManager().getPackageInfo(
+ packageInfo = context.getPackageManager().getPackageInfo(
packageName, PackageManager.GET_SIGNATURES);
} catch (NameNotFoundException e) {
return false;
@@ -416,6 +428,148 @@ public void authorizeCallback(int requestCode, int resultCode, Intent data) {
}
/**
+ * Refresh OAuth access token method. Binds to Facebook for Android
+ * stand-alone application application to refresh the access token. This
+ * method tries to connect to the Facebook App which will handle the
+ * authentication flow, and return a new OAuth access token. This method
+ * will automatically replace the old token with a new one. 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 that will be used to bind to the Facebook
+ * RefreshToken Service
+ * @param serviceListener
+ * Callback interface for notifying the calling application when
+ * the refresh request has completed or failed (can be null). In
+ * case of a success a new token can be found inside the result
+ * Bundle under Facebook.ACCESS_TOKEN key.
+ * @return true if the binding to the RefreshToken Service was created
+ */
+ public boolean extendAccessToken(Context context, ServiceListener serviceListener) {
+ Intent intent = new Intent();
+
+ intent.setClassName("com.facebook.katana",
+ "com.facebook.katana.platform.TokenRefreshService");
+
+ // Verify that the application whose package name is
+ // com.facebook.katana
+ // has the expected FB app signature.
+ if (!validateAppSignatureForIntent(context, intent)) {
+ return false;
+ }
+
+ return context.bindService(intent,
+ new TokenRefreshServiceConnection(context, serviceListener),
+ Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Calls extendAccessToken if shouldExtendAccessToken returns true.
+ *
+ * @return the same value as extendAccessToken if the the token requires
+ * refreshing, true otherwise
+ */
+ public boolean extendAccessTokenIfNeeded(Context context, ServiceListener serviceListener) {
+ if (shouldExtendAccessToken()) {
+ return extendAccessToken(context, serviceListener);
+ }
+ return true;
+ }
+
+ /**
+ * Check if the access token requires refreshing.
+ *
+ * @return true if the last time a new token was obtained was over 24 hours ago.
+ */
+ public boolean shouldExtendAccessToken() {
+ return isSessionValid() &&
+ (System.currentTimeMillis() - mLastAccessUpdate >= REFRESH_TOKEN_BARRIER);
+ }
+
+ /**
+ * Handles connection to the token refresh service (this service is a part
+ * of Facebook App).
+ */
+ private class TokenRefreshServiceConnection implements ServiceConnection {
+
+ final Messenger messageReceiver = new Messenger(new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ String token = msg.getData().getString(TOKEN);
+ long expiresAt = msg.getData().getLong(EXPIRES) * 1000L;
+
+ // To avoid confusion we should return the expiration time in
+ // the same format as the getAccessExpires() function - that
+ // is in milliseconds.
+ Bundle resultBundle = (Bundle) msg.getData().clone();
+ resultBundle.putLong(EXPIRES, expiresAt);
+
+ if (token != null) {
+ setAccessToken(token);
+ setAccessExpires(expiresAt);
+ if (serviceListener != null) {
+ serviceListener.onComplete(resultBundle);
+ }
+ } else if (serviceListener != null) { // extract errors only if client wants them
+ String error = msg.getData().getString("error");
+ if (msg.getData().containsKey("error_code")) {
+ int errorCode = msg.getData().getInt("error_code");
+ serviceListener.onFacebookError(new FacebookError(error, null, errorCode));
+ } else {
+ serviceListener.onError(new Error(error != null ? error
+ : "Unknown service error"));
+ }
+ }
+
+ // The refreshToken function should be called rarely,
+ // so there is no point in keeping the binding open.
+ applicationsContext.unbindService(TokenRefreshServiceConnection.this);
+ }
+ });
+
+ final ServiceListener serviceListener;
+ final Context applicationsContext;
+
+ Messenger messageSender = null;
+
+ public TokenRefreshServiceConnection(Context applicationsContext,
+ ServiceListener serviceListener) {
+ this.applicationsContext = applicationsContext;
+ this.serviceListener = serviceListener;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ messageSender = new Messenger(service);
+ refreshToken();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg) {
+ serviceListener.onError(new Error("Service disconnected"));
+ // We returned an error so there's no point in
+ // keeping the binding open.
+ mAuthActivity.unbindService(TokenRefreshServiceConnection.this);
+ }
+
+ private void refreshToken() {
+ Bundle requestData = new Bundle();
+ requestData.putString(TOKEN, mAccessToken);
+
+ Message request = Message.obtain();
+ request.setData(requestData);
+ request.replyTo = messageReceiver;
+
+ try {
+ messageSender.send(request);
+ } catch (RemoteException e) {
+ serviceListener.onError(new Error("Service connection error"));
+ }
+ }
+ };
+
+ /**
* Invalidate the current user session by removing the access token in
* memory, clearing the browser cookie, and calling auth.expireSession
* through the API.
@@ -673,6 +827,7 @@ public long getAccessExpires() {
*/
public void setAccessToken(String token) {
mAccessToken = token;
+ mLastAccessUpdate = System.currentTimeMillis();
}
/**
@@ -750,6 +905,31 @@ public void setAppId(String appId) {
public void onCancel();
}
+
+ /**
+ * Callback interface for service requests.
+ */
+ public static interface ServiceListener {
+
+ /**
+ * Called when a service request completes.
+ *
+ * @param values
+ * Key-value string pairs extracted from the response.
+ */
+ public void onComplete(Bundle values);
+
+ /**
+ * Called when a Facebook server responds to the request with an error.
+ */
+ public void onFacebookError(FacebookError e);
+
+ /**
+ * Called when a Facebook Service responds to the request with an error.
+ */
+ public void onError(Error e);
+
+ }
public static final String FB_APP_SIGNATURE =
"30820268308201d102044a9c4610300d06092a864886f70d0101040500307a310"
Please sign in to comment.
Something went wrong with that request. Please try again.