package com.github.andlyticsproject.console.v2; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.util.Log; import com.github.andlyticsproject.AndlyticsApp; import com.github.andlyticsproject.R; import com.github.andlyticsproject.console.AuthenticationException; import com.github.andlyticsproject.console.NetworkException; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; import java.io.IOException; public class AccountManagerAuthenticator extends BaseAuthenticator { private static final String TAG = AccountManagerAuthenticator.class.getSimpleName(); private static final String DEVELOPER_CONSOLE_URL = "https://play.google.com/apps/publish/"; private static final Uri ISSUE_AUTH_TOKEN_URL = Uri .parse("https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false"); private static final Uri TOKEN_AUTH_URL = Uri .parse("https://www.google.com/accounts/TokenAuth"); private static final int REQUEST_AUTHENTICATE = 42; private static final boolean DEBUG = false; private AccountManager accountManager; // includes one-time token private String webloginUrl; private DefaultHttpClient httpClient; public AccountManagerAuthenticator(String accountName, DefaultHttpClient httpClient) { super(accountName); this.accountManager = AccountManager.get(AndlyticsApp.getInstance()); this.httpClient = httpClient; } // as described here: http://www.slideshare.net/pickerweng/chromium-os-login // http://www.chromium.org/chromium-os/chromiumos-design-docs/login // and implemented by the Android Browser: // packages/apps/Browser/src/com/android/browser/GoogleAccountLogin.java // packages/apps/Browser/src/com/android/browser/DeviceAccountLogin.java @Override public SessionCredentials authenticate(Activity activity, boolean invalidate) throws AuthenticationException { return authenticateInternal(activity, invalidate); } @Override public SessionCredentials authenticateSilently(boolean invalidate) throws AuthenticationException { return authenticateInternal(null, invalidate); } @SuppressWarnings("deprecation") private SessionCredentials authenticateInternal(Activity activity, boolean invalidate) throws AuthenticationException { try { Account[] accounts = accountManager.getAccountsByType("com.google"); Account account = null; for (Account acc : accounts) { if (acc.name.equals(accountName)) { account = acc; break; } } if (account == null) { throw new AuthenticationException(String.format("Account %s not found on device?", accountName)); } if (invalidate && webloginUrl != null) { // probably not needed, since what we are getting is a very // short-lived token accountManager.invalidateAuthToken(account.type, webloginUrl); } Bundle authResult = accountManager.getAuthToken(account, "weblogin:service=androiddeveloper&continue=" + DEVELOPER_CONSOLE_URL, false, null, null).getResult(); if (authResult.containsKey(AccountManager.KEY_INTENT)) { Intent authIntent = authResult.getParcelable(AccountManager.KEY_INTENT); if (DEBUG) { Log.w(TAG, "Got a reauthenticate intent: " + authIntent); } // silent mode, show notification if (activity == null) { Context ctx = AndlyticsApp.getInstance(); Builder builder = new NotificationCompat.Builder(ctx); builder.setSmallIcon(R.drawable.statusbar_andlytics); builder.setContentTitle(ctx.getResources().getString(R.string.auth_error, accountName)); builder.setContentText(ctx.getResources().getString(R.string.auth_error, accountName)); builder.setAutoCancel(true); PendingIntent contentIntent = PendingIntent.getActivity(ctx, accountName.hashCode(), authIntent, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(contentIntent); NotificationManager nm = (NotificationManager) ctx .getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(accountName.hashCode(), builder.build()); return null; } // activity mode, start activity authIntent.setFlags(authIntent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); activity.startActivityForResult(authIntent, REQUEST_AUTHENTICATE); return null; } webloginUrl = authResult.getString(AccountManager.KEY_AUTHTOKEN); if (webloginUrl == null) { throw new AuthenticationException( "Unexpected authentication error: weblogin URL = null"); } if (DEBUG) { Log.d(TAG, "Weblogin URL: " + webloginUrl); } if (!webloginUrl.contains("MergeSession") || webloginUrl.contains("WILL_NOT_SIGN_IN")) { Log.d(TAG, "Most probably additional verification is required, " + "opening browser"); String sid = accountManager .getAuthToken(account, "SID", null, activity, null, null).getResult() .getString(AccountManager.KEY_AUTHTOKEN); if (sid == null) { throw new AuthenticationException("Authentication error: cannot get SID."); } String lsid = accountManager .getAuthToken(account, "LSID", null, activity, null, null).getResult() .getString(AccountManager.KEY_AUTHTOKEN); if (lsid == null) { throw new AuthenticationException("Authentication error: cannot get LSID."); } String url = ISSUE_AUTH_TOKEN_URL.buildUpon().appendQueryParameter("SID", sid) .appendQueryParameter("LSID", lsid).build().toString(); HttpPost getUberToken = new HttpPost(url); HttpResponse response = httpClient.execute(getUberToken); int status = response.getStatusLine().getStatusCode(); if (status == HttpStatus.SC_UNAUTHORIZED) { throw new AuthenticationException("Cannot get uber token: " + response.getStatusLine()); } String uberToken = EntityUtils.toString(response.getEntity(), "UTF-8"); if (uberToken == null || "".equals(uberToken)) { throw new AuthenticationException("Invalid ubertoken"); } if (DEBUG) { Log.d(TAG, "Uber token: " + uberToken); } webloginUrl = TOKEN_AUTH_URL.buildUpon() .appendQueryParameter("source", "android-browser") .appendQueryParameter("auth", uberToken) .appendQueryParameter("continue", DEVELOPER_CONSOLE_URL).build().toString(); } HttpGet getConsole = new HttpGet(webloginUrl); HttpResponse response = httpClient.execute(getConsole); int status = response.getStatusLine().getStatusCode(); if (status == HttpStatus.SC_UNAUTHORIZED) { throw new AuthenticationException("Authentication token expired: " + response.getStatusLine()); } if (status != HttpStatus.SC_OK) { throw new AuthenticationException("Authentication error: " + response.getStatusLine()); } HttpEntity entity = response.getEntity(); if (entity == null) { throw new AuthenticationException("Authentication error: null result?"); } String responseStr = EntityUtils.toString(entity, "UTF-8"); if (DEBUG) { Log.d(TAG, "Response: " + responseStr); } return createSessionCredentials(accountName, webloginUrl, responseStr, httpClient.getCookieStore().getCookies()); } catch (IOException e) { throw new NetworkException(e); } catch (OperationCanceledException e) { throw new AuthenticationException(e); } catch (AuthenticatorException e) { throw new AuthenticationException(e); } } }