package com.smartandroid.sa.aq; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import org.json.JSONObject; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.SharedPreferences.Editor; 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.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.WebView; import android.webkit.WebViewClient; public class FacebookHandle extends AccountHandle{ private String appId; private Activity act; private WebDialog dialog; private String token; private String permissions; private String message; private static final String OAUTH_ENDPOINT = "https://graph.facebook.com/oauth/authorize"; private static final String REDIRECT_URI = "https://www.facebook.com/connect/login_success.html"; private static final String CANCEL_URI = "fbconnect:cancel"; private boolean first; private boolean sso; private int requestId; public FacebookHandle(Activity act, String appId, String permissions) { this(act, appId, permissions, null); } public FacebookHandle(Activity act, String appId, String permissions, String accessToken) { this.appId = appId; this.act = act; this.permissions = permissions; this.token = accessToken; if(token == null && permissionOk(permissions, fetchPermission())){ token = fetchToken(); } first = token == null; } public String getToken(){ return token; } public static String getToken(Context context){ return PreferenceManager.getDefaultSharedPreferences(context).getString(FB_TOKEN, null); } public FacebookHandle sso(int requestId){ this.sso = true; this.requestId = requestId; return this; } private boolean permissionOk(String permissions, String old){ if(permissions == null) return true; if(old == null) return false; String[] splits = old.split("[,\\s]+"); Set<String> oldSet = new HashSet<String>(Arrays.asList(splits)); splits = permissions.split("[,\\s]+"); for(int i = 0; i < splits.length; i++){ if(!oldSet.contains(splits[i])){ AQUtility.debug("perm mismatch"); return false; } } return true; } public FacebookHandle message(String message){ this.message = message; return this; } public FacebookHandle setLoadingMessage(int resId){ this.message = act.getString(resId); return this; } private void dismiss(){ if(dialog != null){ new AQuery(act).dismiss(dialog); dialog = null; } } private void show(){ if(dialog != null){ new AQuery(act).show(dialog); } } private void hide(){ if(dialog != null){ try{ dialog.hide(); }catch(Exception e){ AQUtility.debug(e); } } } private void failure(){ failure("cancel"); } private void failure(String message){ dismiss(); failure(act, AjaxStatus.AUTH_ERROR, message); } protected void auth() { if(act.isFinishing()) return; boolean ok = sso(); AQUtility.debug("authing", ok); if(!ok){ webAuth(); } } private boolean sso(){ if(!sso) return false; return startSingleSignOn(act, appId, permissions, requestId); } private void webAuth() { AQUtility.debug("web auth"); Bundle parameters = new Bundle(); parameters.putString("client_id", appId); parameters.putString("type", "user_agent"); if(permissions != null){ parameters.putString("scope", permissions); } parameters.putString("redirect_uri", REDIRECT_URI); String url = OAUTH_ENDPOINT + "?" + encodeUrl(parameters); FbWebViewClient client = new FbWebViewClient(); dialog = new WebDialog(act, url, client); dialog.setLoadingMessage(message); dialog.setOnCancelListener(client); show(); if(!first || token != null){ AQUtility.debug("auth hide"); hide(); } dialog.load(); AQUtility.debug("auth started"); } private static final String FB_TOKEN = "aq.fb.token"; private static final String FB_PERMISSION = "aq.fb.permission"; private String fetchToken(){ return PreferenceManager.getDefaultSharedPreferences(act).getString(FB_TOKEN, null); } private String fetchPermission(){ return PreferenceManager.getDefaultSharedPreferences(act).getString(FB_PERMISSION, null); } private void storeToken(String token, String permission){ Editor editor = PreferenceManager.getDefaultSharedPreferences(act).edit(); editor.putString(FB_TOKEN, token).putString(FB_PERMISSION, permission);//.commit(); AQUtility.apply(editor); } private class FbWebViewClient extends WebViewClient implements OnCancelListener { private boolean checkDone(String url){ if(url.startsWith(REDIRECT_URI)) { Bundle values = parseUrl(url); String error = values.getString("error_reason"); AQUtility.debug("error", error); if(error == null) { token = extractToken(url); } if(token != null){ dismiss(); storeToken(token, permissions); first = false; authenticated(token); success(act); }else{ failure(); } return true; }else if(url.startsWith(CANCEL_URI)) { AQUtility.debug("cancelled"); failure(); return true; } return false; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { AQUtility.debug("return url: " + url); return checkDone(url); } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { AQUtility.debug("started", url); if(checkDone(url)){ }else{ super.onPageStarted(view, url, favicon); } } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); show(); AQUtility.debug("finished", url); } @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { failure(); } @Override public void onCancel(DialogInterface dialog) { failure(); } } private String extractToken(String url) { Uri uri = Uri.parse(url.replace('#', '?')); String token = uri.getQueryParameter("access_token"); AQUtility.debug("token", token); return token; } private 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(key + "=" + parameters.getString(key)); } return sb.toString(); } private 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(v[0], v[1]); } } return params; } private static Bundle parseUrl(String url) { try { URL u = new URL(url); Bundle b = decodeUrl(u.getQuery()); b.putAll(decodeUrl(u.getRef())); return b; } catch (MalformedURLException e) { return new Bundle(); } } //03-17 17:05:40.594: W/AQuery(23190): error:{"error":{"message":"Error validating access token: User 1318428934 has not authorized application 155734287864315.","type":"OAuthException","code":190}} @Override public boolean expired(AbstractAjaxCallback<?, ?> cb, AjaxStatus status) { int code = status.getCode(); if(code == 200) return false; String error = status.getError(); if(error != null && error.contains("OAuthException")){ AQUtility.debug("fb token expired"); return true; } String url = cb.getUrl(); if(code == 400 && (url.endsWith("/likes") || url.endsWith("/comments") || url.endsWith("/checkins"))){ return false; } if(code == 403 && (url.endsWith("/feed") || url.contains("method=delete"))){ return false; } return code == 400 || code == 401 || code == 403; } @Override public boolean reauth(final AbstractAjaxCallback<?, ?> cb) { AQUtility.debug("reauth requested"); token = null; AQUtility.post(new Runnable() { @Override public void run() { auth(cb); } }); return false; } @Override public String getNetworkUrl(String url){ if(url.indexOf('?') == -1){ url = url + "?"; }else{ url = url + "&"; } url = url + "access_token=" + token; return url; } @Override public String getCacheUrl(String url){ return getNetworkUrl(url); } @Override public boolean authenticated() { return token != null; } @Override public void unauth(){ token = null; CookieSyncManager.createInstance(act); CookieManager.getInstance().removeAllCookie(); storeToken(null, null); } 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 != null) { intent.putExtra("scope", permissions); } if(!validateAppSignatureForIntent(activity, intent)){ return false; } try { activity.startActivityForResult(intent, activityCode); } catch (ActivityNotFoundException e) { didSucceed = false; } return didSucceed; } private static Boolean hasSSO; public boolean isSSOAvailable(){ if(hasSSO == null){ Intent intent = new Intent(); intent.setClassName("com.facebook.katana", "com.facebook.katana.ProxyAuth"); hasSSO = validateAppSignatureForIntent(act, intent); } return hasSSO; } protected void authenticated(String token){ } public void ajaxProfile(AjaxCallback<JSONObject> cb){ ajaxProfile(cb, 0); } public void ajaxProfile(AjaxCallback<JSONObject> cb, long expire){ String url = "https://graph.facebook.com/me"; AQuery aq = new AQuery(act); aq.auth(this).ajax(url, JSONObject.class, expire, cb); } private boolean validateAppSignatureForIntent(Context context, Intent intent) { PackageManager pm = context.getPackageManager(); ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); if(resolveInfo == null){ return false; } String packageName = resolveInfo.activityInfo.packageName; PackageInfo packageInfo; try { packageInfo = pm.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; } public void onActivityResult(int requestCode, int resultCode, Intent data) { AQUtility.debug("on result", resultCode); // 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) { AQUtility.debug("error", error); if(error.equals("service_disabled") || error.equals("AndroidAuthKillSwitchException")) { webAuth(); }else{ String description = data.getStringExtra("error_description"); AQUtility.debug("fb error", description); Log.e("fb error", description); failure(description); } // No errors. }else{ token = data.getStringExtra("access_token"); AQUtility.debug("onComplete", token); if(token != null){ storeToken(token, permissions); first = false; authenticated(token); success(act); }else{ failure(); } } // An error occurred before we could be redirected. }else if (resultCode == Activity.RESULT_CANCELED) { failure(); } } 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"; }