Skip to content

Commit

Permalink
[android-sdk] Adds RequestBatch-level completion callback; fixes Sess…
Browse files Browse the repository at this point in the history
…ion reauth behavior.

Summary:
Some scenarios involving batched Graph API calls are more cleanly implemented with a completion callback that applies
to the batch as a whole, rather than individual requests within the batch. This commit adds such a callback to
RequestBatch.

It also fixes a couple issues with Session and reauthorization. It adds a check to ensure that the access token resulting
from a reauthorization represents the same Facebook profile as the previous access token did (since users could authenticate
using different credentials than when the session was originally opened). It also fetches a current list of permissions
for the user following a reauthorization, to help ensure that the client copy of the permission list stays in sync with
the service.

Finally, it fixes LoginActivity to pass back the AccessTokenSource of the access token it provides, so we can set it properly.

Test Plan:
- Added unit tests
- Ran unit tests
- Ran Scrumptious, HelloFacebook, and BooleanOG samples

Revert Plan:

Reviewers: mmarucheck, mingfli, karthiks

Reviewed By: mmarucheck

CC: ekoneil, caabernathy

Differential Revision: https://phabricator.fb.com/D647716

Task ID: 1925038, 1925028
  • Loading branch information
Chris Lang committed Dec 7, 2012
1 parent 3afe006 commit 5ca08f4
Show file tree
Hide file tree
Showing 17 changed files with 708 additions and 221 deletions.
24 changes: 9 additions & 15 deletions facebook/src/com/facebook/AccessToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import com.facebook.internal.Utility;
import com.facebook.internal.Validate;

Expand Down Expand Up @@ -198,18 +199,11 @@ static AccessToken createFromNativeLogin(Intent data) {
return createNew(permissions, token, expires, AccessTokenSource.FACEBOOK_APPLICATION_NATIVE);
}

static AccessToken createFromDialog(List<String> requestedPermissions, Bundle bundle) {
static AccessToken createFromWebBundle(List<String> requestedPermissions, Bundle bundle, AccessTokenSource source) {
Date expires = getBundleLongAsDate(bundle, EXPIRES_IN_KEY, new Date());
String token = bundle.getString(ACCESS_TOKEN_KEY);

return createNew(requestedPermissions, token, expires, AccessTokenSource.WEB_VIEW);
}

static AccessToken createFromWebSSO(List<String> requestedPermissions, Intent data) {
Date expires = getBundleLongAsDate(data.getExtras(), EXPIRES_IN_KEY, new Date());
String token = data.getStringExtra(ACCESS_TOKEN_KEY);

return createNew(requestedPermissions, token, expires, AccessTokenSource.FACEBOOK_APPLICATION_WEB);
return createNew(requestedPermissions, token, expires, source);
}

@SuppressLint("FieldGetter")
Expand All @@ -225,6 +219,10 @@ static AccessToken createFromRefresh(AccessToken current, Bundle bundle) {
return createNew(current.getPermissions(), token, expires, current.source);
}

static AccessToken createFromTokenWithRefreshedPermissions(AccessToken token, List<String> permissions) {
return new AccessToken(token.token, token.expires, permissions, token.source, token.lastRefresh);
}

private static AccessToken createNew(
List<String> requestedPermissions, String accessToken, Date expires, AccessTokenSource source) {
if (Utility.isNullOrEmpty(accessToken) || (expires == null)) {
Expand Down Expand Up @@ -294,12 +292,8 @@ private void appendPermissions(StringBuilder builder) {
builder.append("null");
} else {
builder.append("[");
for (int i = 0; i < this.permissions.size(); i++) {
if (i > 0) {
builder.append(", ");
}
builder.append(this.permissions.get(i));
}
builder.append(TextUtils.join(", ", permissions));
builder.append("]");
}
}

Expand Down
104 changes: 25 additions & 79 deletions facebook/src/com/facebook/LoginActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.text.TextUtils;
import android.webkit.CookieSyncManager;
Expand All @@ -35,6 +34,7 @@
import com.facebook.widget.WebDialog;

import java.util.ArrayList;
import java.util.List;

/**
* This class addresses the issue of a potential window leak during
Expand All @@ -50,11 +50,10 @@ public class LoginActivity extends Activity {
static final String EXTRA_IS_LEGACY = "com.facebook.sdk.extra.IS_LEGACY";
static final String EXTRA_DEFAULT_AUDIENCE = "com.facebook.sdk.extra.DEFAULT_AUDIENCE";

private static final String BASIC_INFO = "basic_info";

static final String LOGIN_FAILED = "Login attempt failed.";
static final String INTERNET_PERMISSIONS_NEEDED = "WebView login requires INTERNET permission";
static final String ERROR_KEY = "error";
static final String ACCESS_TOKEN_SOURCE_KEY = "com.facebook.LoginActivity:AccessTokenSource";

private static final int DEFAULT_REQUEST_CODE = 0xface;
private static final String NULL_CALLING_PKG_ERROR_MSG =
Expand Down Expand Up @@ -129,12 +128,16 @@ public void onSaveInstanceState(Bundle outState) {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DEFAULT_REQUEST_CODE) {
if (isServiceDisabledResult20121101(data)) {
if (NativeProtocol.isServiceDisabledResult20121101(data)) {
// Fall back to legacy auth
isLegacy = true;
startedKatana = false;
startAuth();
} else {
Bundle extras = data.getExtras();
AccessTokenSource source = NativeProtocol.getAccessTokenSourceFromNative(extras);
data.putExtra(ACCESS_TOKEN_SOURCE_KEY, source.name());

setResult(resultCode, data);
finish();
}
Expand All @@ -143,10 +146,10 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {

private void startAuth() {
boolean started = startedKatana;
if (!started && allowKatana(loginBehavior)) {
if (!started && loginBehavior.allowsKatanaAuth()) {
started = tryKatanaAuth();
}
if (!started && allowWebView(loginBehavior)) {
if (!started && loginBehavior.allowsWebViewAuth()) {
started = tryDialogAuth();
}
if (!started) {
Expand Down Expand Up @@ -190,84 +193,34 @@ static Intent getProxyAuthIntent(Context context, Bundle extras) {
String applicationId = extras.getString(EXTRA_APPLICATION_ID);
ArrayList<String> permissions = extras.getStringArrayList(EXTRA_PERMISSIONS);

Intent intent = new Intent()
.setClassName(NativeProtocol.KATANA_PACKAGE, NativeProtocol.KATANA_PROXY_AUTH_ACTIVITY)
.putExtra(ServerProtocol.DIALOG_PARAM_CLIENT_ID, applicationId);

if (!Utility.isNullOrEmpty(permissions)) {
intent.putExtra(ServerProtocol.DIALOG_PARAM_SCOPE, TextUtils.join(",", permissions));
}

return validateKatanaIntent(context, intent);
return NativeProtocol.createProxyAuthIntent(context, applicationId, permissions);
}

static Intent getLoginDialog20121101Intent(Context context, Bundle extras) {
String applicationId = extras.getString(EXTRA_APPLICATION_ID);
ArrayList<String> permissions = extras.getStringArrayList(EXTRA_PERMISSIONS);
String audience = extras.getString(EXTRA_DEFAULT_AUDIENCE);

Intent intent = new Intent()
.setAction(NativeProtocol.INTENT_ACTION_PLATFORM_ACTIVITY)
.addCategory(Intent.CATEGORY_DEFAULT)
.putExtra(NativeProtocol.EXTRA_PROTOCOL_VERSION, NativeProtocol.PROTOCOL_VERSION_20121101)
.putExtra(NativeProtocol.EXTRA_PROTOCOL_ACTION, NativeProtocol.ACTION_LOGIN_DIALOG)
.putExtra(NativeProtocol.EXTRA_APPLICATION_ID, applicationId)
.putStringArrayListExtra(NativeProtocol.EXTRA_PERMISSIONS, ensureDefaultPermissions(permissions))
.putExtra(NativeProtocol.EXTRA_WRITE_PRIVACY, ensureDefaultAudience(audience));

return validateKatanaIntent(context, intent);
}

private static String ensureDefaultAudience(String audience) {
if (Utility.isNullOrEmpty(audience)) {
return NativeProtocol.AUDIENCE_ME;
} else {
return audience;
}
}

private static ArrayList<String> ensureDefaultPermissions(ArrayList<String> permissions) {
ArrayList<String> updated;

// Return if we are doing publish, or if basic_info is already included
if (Utility.isNullOrEmpty(permissions)) {
updated = new ArrayList<String>();
} else {
for (String permission : permissions) {
if (Session.isPublishPermission(permission) || BASIC_INFO.equals(permission)) {
return permissions;
}
}
updated = new ArrayList<String>(permissions);
}

updated.add(BASIC_INFO);
return updated;
return NativeProtocol.createLoginDialog20121101Intent(context, applicationId, permissions, audience);
}

private boolean isServiceDisabledResult20121101(Intent data) {
int protocolVersion = data.getIntExtra(NativeProtocol.EXTRA_PROTOCOL_VERSION, 0);
String errorType = data.getStringExtra(NativeProtocol.STATUS_ERROR_TYPE);

return ((NativeProtocol.PROTOCOL_VERSION_20121101 == protocolVersion) &&
NativeProtocol.ERROR_SERVICE_DISABLED.equals(errorType));
}

private static Intent validateKatanaIntent(Context context, Intent intent) {
if (intent == null) {
return null;
// Populates a Bundle with extras suitable for starting LoginActivity.
static Bundle populateIntentExtras(String applicationId, boolean isLegacy, SessionDefaultAudience audience,
List<String> permissions) {
Bundle extras = new Bundle();
extras.putString(LoginActivity.EXTRA_APPLICATION_ID, applicationId);
if (isLegacy) {
extras.putBoolean(LoginActivity.EXTRA_IS_LEGACY, true);
}

ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
if (resolveInfo == null) {
return null;
String audienceString = audience.getNativeProtocolAudience();
if (audienceString != null) {
extras.putString(LoginActivity.EXTRA_DEFAULT_AUDIENCE, audienceString);
}

if (!NativeProtocol.validateSignature(context, resolveInfo.activityInfo.packageName)) {
return null;
if (!Utility.isNullOrEmpty(permissions)) {
extras.putStringArrayList(LoginActivity.EXTRA_PERMISSIONS, new ArrayList<String>(permissions));
}

return intent;
return extras;
}

private boolean tryDialogAuth() {
Expand Down Expand Up @@ -315,6 +268,7 @@ public void onComplete(Bundle values, FacebookException error) {
if (values != null) {
// Ensure any cookies set by the dialog are saved
CookieSyncManager.getInstance().sync();
values.putString(ACCESS_TOKEN_SOURCE_KEY, AccessTokenSource.WEB_VIEW.name());
finishWithResultOk(values);
} else {
Bundle bundle = new Bundle();
Expand All @@ -340,14 +294,6 @@ public void onComplete(Bundle values, FacebookException error) {
return true;
}

static boolean allowKatana(SessionLoginBehavior loginBehavior) {
return !SessionLoginBehavior.SUPPRESS_SSO.equals(loginBehavior);
}

static boolean allowWebView(SessionLoginBehavior loginBehavior) {
return !SessionLoginBehavior.SSO_ONLY.equals(loginBehavior);
}

private void finishWithResultOk(Bundle extras) {
finishWithResult(true, extras);
}
Expand Down
124 changes: 123 additions & 1 deletion facebook/src/com/facebook/NativeProtocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@
package com.facebook;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.os.Bundle;
import android.text.TextUtils;
import com.facebook.internal.Utility;

import java.util.ArrayList;
import java.util.List;

final class NativeProtocol {
static final String KATANA_PACKAGE = "com.facebook.katana";
Expand All @@ -46,6 +54,9 @@ final class NativeProtocol {
+ "73149fb2232a10d247663b26a9031e15f84bc1c74d141ff98a02d76f85b2c8ab2"
+ "571b6469b232d8e768a7f7ca04f7abe4a775615916c07940656b58717457b42bd"
+ "928a2";
private static final String BASIC_INFO = "basic_info";
public static final String KATANA_PROXY_AUTH_PERMISSIONS_KEY = "scope";
public static final String KATANA_PROXY_AUTH_APP_ID_KEY = "client_id";

static final boolean validateSignature(Context context, String packageName) {
PackageInfo packageInfo = null;
Expand All @@ -57,14 +68,67 @@ static final boolean validateSignature(Context context, String packageName) {
}

for (Signature signature : packageInfo.signatures) {
if (signature.toCharsString().equals(NativeProtocol.KATANA_SIGNATURE)) {
if (signature.toCharsString().equals(KATANA_SIGNATURE)) {
return true;
}
}

return false;
}

static Intent validateKatanaActivityIntent(Context context, Intent intent) {
if (intent == null) {
return null;
}

ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
if (resolveInfo == null) {
return null;
}

if (!validateSignature(context, resolveInfo.activityInfo.packageName)) {
return null;
}

return intent;
}

static Intent validateKatanaServiceIntent(Context context, Intent intent) {
if (intent == null) {
return null;
}

ResolveInfo resolveInfo = context.getPackageManager().resolveService(intent, 0);
if (resolveInfo == null) {
return null;
}

if (!validateSignature(context, resolveInfo.serviceInfo.packageName)) {
return null;
}

return intent;
}

static Intent createProxyAuthIntent(Context context, String applicationId, List<String> permissions) {
Intent intent = new Intent()
.setClassName(KATANA_PACKAGE, KATANA_PROXY_AUTH_ACTIVITY)
.putExtra(KATANA_PROXY_AUTH_APP_ID_KEY, applicationId);

if (!Utility.isNullOrEmpty(permissions)) {
intent.putExtra(KATANA_PROXY_AUTH_PERMISSIONS_KEY, TextUtils.join(",", permissions));
}

return validateKatanaActivityIntent(context, intent);
}

static Intent createTokenRefreshIntent(Context context) {
Intent intent = new Intent();
intent.setClassName(KATANA_PACKAGE, KATANA_TOKEN_REFRESH_ACTIVITY);

return validateKatanaServiceIntent(context, intent);
}

// ---------------------------------------------------------------------------------------------
// Native Protocol updated 2012-11

Expand Down Expand Up @@ -114,4 +178,62 @@ static final boolean validateSignature(Context context, String packageName) {
public static final String AUDIENCE_ME = "SELF";
public static final String AUDIENCE_FRIENDS = "ALL_FRIENDS";
public static final String AUDIENCE_EVERYONE = "EVERYONE";

static Intent createLoginDialog20121101Intent(Context context, String applicationId, ArrayList<String> permissions,
String audience) {
Intent intent = new Intent()
.setAction(INTENT_ACTION_PLATFORM_ACTIVITY)
.addCategory(Intent.CATEGORY_DEFAULT)
.putExtra(EXTRA_PROTOCOL_VERSION, PROTOCOL_VERSION_20121101)
.putExtra(EXTRA_PROTOCOL_ACTION, ACTION_LOGIN_DIALOG)
.putExtra(EXTRA_APPLICATION_ID, applicationId)
.putStringArrayListExtra(EXTRA_PERMISSIONS, ensureDefaultPermissions(permissions))
.putExtra(EXTRA_WRITE_PRIVACY, ensureDefaultAudience(audience));
return validateKatanaActivityIntent(context, intent);
}

private static String ensureDefaultAudience(String audience) {
if (Utility.isNullOrEmpty(audience)) {
return AUDIENCE_ME;
} else {
return audience;
}
}

private static ArrayList<String> ensureDefaultPermissions(ArrayList<String> permissions) {
ArrayList<String> updated;

// Return if we are doing publish, or if basic_info is already included
if (Utility.isNullOrEmpty(permissions)) {
updated = new ArrayList<String>();
} else {
for (String permission : permissions) {
if (Session.isPublishPermission(permission) || BASIC_INFO.equals(permission)) {
return permissions;
}
}
updated = new ArrayList<String>(permissions);
}

updated.add(BASIC_INFO);
return updated;
}

static boolean isServiceDisabledResult20121101(Intent data) {
int protocolVersion = data.getIntExtra(EXTRA_PROTOCOL_VERSION, 0);
String errorType = data.getStringExtra(STATUS_ERROR_TYPE);

return ((PROTOCOL_VERSION_20121101 == protocolVersion) && ERROR_SERVICE_DISABLED.equals(errorType));
}

static AccessTokenSource getAccessTokenSourceFromNative(Bundle extras) {
long expected = PROTOCOL_VERSION_20121101;
long actual = extras.getInt(EXTRA_PROTOCOL_VERSION, 0);

if (expected == actual) {
return AccessTokenSource.FACEBOOK_APPLICATION_NATIVE;
} else {
return AccessTokenSource.FACEBOOK_APPLICATION_WEB;
}
}
}
Loading

0 comments on commit 5ca08f4

Please sign in to comment.