diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 89d130b59..3f2b6f595 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -62,7 +62,7 @@ - + diff --git a/res/values/keys.xml b/res/values/keys.xml new file mode 100644 index 000000000..62eca7ad9 --- /dev/null +++ b/res/values/keys.xml @@ -0,0 +1,4 @@ + + + 959061759285-uqnem9qtjr97856o7b6pkuek0ref5dnd.apps.googleusercontent.com + diff --git a/src/main/java/com/zegoggles/smssync/activity/MainActivity.java b/src/main/java/com/zegoggles/smssync/activity/MainActivity.java index 090beff80..1dd87de2c 100644 --- a/src/main/java/com/zegoggles/smssync/activity/MainActivity.java +++ b/src/main/java/com/zegoggles/smssync/activity/MainActivity.java @@ -53,8 +53,9 @@ import com.zegoggles.smssync.Consts; import com.zegoggles.smssync.R; import com.zegoggles.smssync.activity.auth.AccountManagerAuthActivity; -import com.zegoggles.smssync.activity.auth.WebAuthActivity; +import com.zegoggles.smssync.activity.auth.OAuth2WebAuthActivity; import com.zegoggles.smssync.activity.donation.DonationActivity; +import com.zegoggles.smssync.auth.OAuth2Client; import com.zegoggles.smssync.calendar.CalendarAccessor; import com.zegoggles.smssync.contacts.ContactAccessor; import com.zegoggles.smssync.mail.BackupImapStore; @@ -69,11 +70,9 @@ import com.zegoggles.smssync.service.SmsBackupService; import com.zegoggles.smssync.service.SmsRestoreService; import com.zegoggles.smssync.service.state.RestoreState; -import com.zegoggles.smssync.tasks.OAuthCallbackTask; -import com.zegoggles.smssync.tasks.RequestTokenTask; +import com.zegoggles.smssync.tasks.OAuth2CallbackTask; import com.zegoggles.smssync.utils.AppLog; import com.zegoggles.smssync.utils.ListPreferenceHelper; -import org.jetbrains.annotations.Nullable; import java.text.DateFormat; import java.util.ArrayList; @@ -82,8 +81,22 @@ import java.util.Locale; import static com.zegoggles.smssync.App.TAG; -import static com.zegoggles.smssync.mail.DataType.*; -import static com.zegoggles.smssync.preferences.Preferences.Keys.*; +import static com.zegoggles.smssync.mail.DataType.CALLLOG; +import static com.zegoggles.smssync.mail.DataType.MMS; +import static com.zegoggles.smssync.mail.DataType.SMS; +import static com.zegoggles.smssync.preferences.Preferences.Keys.BACKUP_CONTACT_GROUP; +import static com.zegoggles.smssync.preferences.Preferences.Keys.BACKUP_SETTINGS_SCREEN; +import static com.zegoggles.smssync.preferences.Preferences.Keys.CALLLOG_SYNC_CALENDAR; +import static com.zegoggles.smssync.preferences.Preferences.Keys.CALLLOG_SYNC_CALENDAR_ENABLED; +import static com.zegoggles.smssync.preferences.Preferences.Keys.CONNECTED; +import static com.zegoggles.smssync.preferences.Preferences.Keys.DONATE; +import static com.zegoggles.smssync.preferences.Preferences.Keys.ENABLE_AUTO_BACKUP; +import static com.zegoggles.smssync.preferences.Preferences.Keys.IMAP_SETTINGS; +import static com.zegoggles.smssync.preferences.Preferences.Keys.INCOMING_TIMEOUT_SECONDS; +import static com.zegoggles.smssync.preferences.Preferences.Keys.MAX_ITEMS_PER_RESTORE; +import static com.zegoggles.smssync.preferences.Preferences.Keys.MAX_ITEMS_PER_SYNC; +import static com.zegoggles.smssync.preferences.Preferences.Keys.REGULAR_TIMEOUT_SECONDS; +import static com.zegoggles.smssync.preferences.Preferences.Keys.WIFI_ONLY; /** * This is the main activity showing the status of the SMS Sync service and @@ -106,12 +119,13 @@ enum Actions { private AuthPreferences authPreferences; private Preferences preferences; private StatusPreference statusPref; - private @Nullable Uri mAuthorizeUri; + private OAuth2Client oauth2Client; @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); authPreferences = new AuthPreferences(this); + oauth2Client = new OAuth2Client(authPreferences.getOAuth2ClientId()); preferences = new Preferences(this); addPreferencesFromResource(R.xml.preferences); @@ -220,10 +234,12 @@ public boolean onOptionsItemSelected(MenuItem item) { break; } case REQUEST_WEB_AUTH: { - Uri uri = data.getData(); - if (uri != null && uri.toString().startsWith(Consts.CALLBACK_URL)) { + final String code = data.getStringExtra(OAuth2WebAuthActivity.EXTRA_CODE); + if (!TextUtils.isEmpty(code)) { show(Dialogs.ACCESS_TOKEN); - new OAuthCallbackTask(this).execute(data); + new OAuth2CallbackTask(oauth2Client).execute(code); + } else { + show(Dialogs.ACCESS_TOKEN_ERROR); } break; } @@ -244,21 +260,10 @@ public boolean onOptionsItemSelected(MenuItem item) { } } - @Subscribe public void onAuthorizedURLReceived(RequestTokenTask.AuthorizedURLReceived authorizedURLReceived) { - dismiss(Dialogs.REQUEST_TOKEN); - this.mAuthorizeUri = authorizedURLReceived.uri; - if (mAuthorizeUri != null) { - show(Dialogs.CONNECT); - } else { - show(Dialogs.CONNECT_TOKEN_ERROR); - } - } - - @Subscribe public void onOAuthCallback(OAuthCallbackTask.OAuthCallbackEvent event) { + @Subscribe public void onOAuth2Callback(OAuth2CallbackTask.OAuth2CallbackEvent event) { dismiss(Dialogs.ACCESS_TOKEN); if (event.valid()) { - authPreferences.setOauthUsername(event.username); - authPreferences.setOauthTokens(event.token, event.tokenSecret); + authPreferences.setOauth2Token(event.token.userName, event.token.accessToken, event.token.refreshToken); onAuthenticated(); } else { show(Dialogs.ACCESS_TOKEN_ERROR); @@ -614,10 +619,9 @@ public void onClick(DialogInterface dialog, int which) { .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - if (mAuthorizeUri != null) { - startActivityForResult(new Intent(MainActivity.this, WebAuthActivity.class) - .setData(mAuthorizeUri), REQUEST_WEB_AUTH); - } + startActivityForResult(new Intent(MainActivity.this, OAuth2WebAuthActivity.class) + .setData(oauth2Client.requestUrl()), REQUEST_WEB_AUTH); + dismissDialog(id); } }).create(); @@ -957,7 +961,7 @@ private void handleAccountManagerAuth(Intent data) { String token = data.getStringExtra(AccountManagerAuthActivity.EXTRA_TOKEN); String account = data.getStringExtra(AccountManagerAuthActivity.EXTRA_ACCOUNT); if (!TextUtils.isEmpty(token) && !TextUtils.isEmpty(account)) { - authPreferences.setOauth2Token(account, token); + authPreferences.setOauth2Token(account, token, null); onAuthenticated(); } else { String error = data.getStringExtra(AccountManagerAuthActivity.EXTRA_ERROR); @@ -968,8 +972,7 @@ private void handleAccountManagerAuth(Intent data) { } private void handleFallbackAuth() { - show(Dialogs.REQUEST_TOKEN); - new RequestTokenTask(this).execute(Consts.CALLBACK_URL); + show(Dialogs.CONNECT); } private void checkDefaultSmsApp() { diff --git a/src/main/java/com/zegoggles/smssync/activity/auth/WebAuthActivity.java b/src/main/java/com/zegoggles/smssync/activity/auth/OAuth2WebAuthActivity.java similarity index 71% rename from src/main/java/com/zegoggles/smssync/activity/auth/WebAuthActivity.java rename to src/main/java/com/zegoggles/smssync/activity/auth/OAuth2WebAuthActivity.java index 340d0c626..65554d1ff 100644 --- a/src/main/java/com/zegoggles/smssync/activity/auth/WebAuthActivity.java +++ b/src/main/java/com/zegoggles/smssync/activity/auth/OAuth2WebAuthActivity.java @@ -11,6 +11,7 @@ import android.net.http.SslError; import android.os.Build; import android.os.Bundle; +import android.text.TextUtils; import android.util.Log; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; @@ -18,13 +19,24 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import com.zegoggles.smssync.App; -import com.zegoggles.smssync.Consts; import com.zegoggles.smssync.R; -public class WebAuthActivity extends Activity { +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.zegoggles.smssync.App.TAG; + +public class OAuth2WebAuthActivity extends Activity { private WebView mWebview; private ProgressDialog mProgress; + public static final String EXTRA_CODE = "code"; + public static final String EXTRA_ERROR = "error"; + + // Success code=4/8imH8gQubRYrWu_Fpv6u4Yri5kTNEWmm_XyhytJqlJw + // Denied error=access_denied + private static final Pattern TITLE = Pattern.compile("(code|error)=(.+)\\Z"); + public void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView(R.layout.auth_activity); @@ -51,7 +63,7 @@ public void onReceivedError(WebView view, int errorCode, String description, Str @Override @TargetApi(Build.VERSION_CODES.FROYO) public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - Log.w(App.TAG, "onReceiveSslError(" + error + ")"); + Log.w(TAG, "onReceiveSslError(" + error + ")"); // pre-froyo devices don't trust the cert used by google // see https://knowledge.verisign.com/support/mpki-for-ssl-support/index?page=content&id=SO17511&actp=AGENT_REFERAL if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO && @@ -65,32 +77,45 @@ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError e @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - if (App.LOCAL_LOGV) Log.d(App.TAG, "onPageStarted(" + url + ")"); + if (App.LOCAL_LOGV) Log.d(TAG, "onPageStarted(" + url + ")"); if (!isFinishing()) mProgress.show(); } @Override public void onPageFinished(WebView view, String url) { - if (!isFinishing()) mProgress.dismiss(); - } + final String pageTitle = view.getTitle(); + final Matcher matcher = TITLE.matcher(pageTitle); + if (matcher.find()) { + String status = matcher.group(1); + String value = matcher.group(2); - @Override - public boolean shouldOverrideUrlLoading(final WebView view, String url) { - if (url.startsWith(Consts.CALLBACK_URL)) { - setResult(RESULT_OK, new Intent().setData(Uri.parse(url))); - finish(); - return true; - } else { - return false; + if ("code".equals(status)) { + onCodeReceived(value); + } else if ("error".equals(status)) { + onError(value); + } } + if (!isFinishing()) mProgress.dismiss(); } }); removeAllCookies(); - // finally load url mWebview.loadUrl(urlToLoad.toString()); } + private void onCodeReceived(String code) { + if (!TextUtils.isEmpty(code)) { + setResult(RESULT_OK, new Intent().putExtra(EXTRA_CODE, code)); + finish(); + } + } + + private void onError(String error) { + Log.e(TAG, "onError("+error+")"); + setResult(RESULT_OK, new Intent().putExtra(EXTRA_ERROR, error)); + finish(); + } + private void showConnectionError(final String message) { if (isFinishing()) return; new AlertDialog.Builder(this). diff --git a/src/main/java/com/zegoggles/smssync/auth/OAuth2Client.java b/src/main/java/com/zegoggles/smssync/auth/OAuth2Client.java new file mode 100644 index 000000000..424573bb7 --- /dev/null +++ b/src/main/java/com/zegoggles/smssync/auth/OAuth2Client.java @@ -0,0 +1,280 @@ +package com.zegoggles.smssync.auth; + +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +import javax.net.ssl.HttpsURLConnection; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +import static com.zegoggles.smssync.App.TAG; + +/** + * https://developers.google.com/identity/protocols/OAuth2UserAgent + */ +public class OAuth2Client { + private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/auth"; + public static final String TOKEN_URL = "https://www.googleapis.com/oauth2/v3/token"; + + /** + * For installed applications, use a value of code, indicating that the Google OAuth 2.0 endpoint should return an authorization code. + */ + private static final String RESPONSE_TYPE = "response_type"; + + /** + * Identifies the client that is making the request. + * The value passed in this parameter must exactly match the value shown in the Google Developers Console. + */ + private static final String CLIENT_ID = "client_id"; + + /** + * Determines where the response is sent. + * The value of this parameter must exactly match one of the values that appear in the + * Credentials page in the Google Developers Console (including the http or https scheme, case, and trailing slash). + * You may choose between urn:ietf:wg:oauth:2.0:oob, + * urn:ietf:wg:oauth:2.0:oob:auto, or an http://localhost port. + * For more details, see Choosing a redirect URI. + */ + private static final String REDIRECT_URI = "redirect_uri"; + + /** + * Space-delimited set of scope strings. + * + * Identifies the Google API access that your application is requesting. + * The values passed in this parameter inform the consent screen that is shown to the user. There may be an inverse + * relationship between the number of permissions requested and the likelihood + * of obtaining user consent. + */ + private static final String SCOPE = "scope"; + + /** + * Provides any state information that might be useful to your application upon receipt + * of the response. The Google Authorization Server roundtrips this parameter, so your application receives + * the same value it sent. Possible uses include redirecting the user to the + * correct resource in your site, nonces, and cross-site-request-forgery mitigations. + */ + private static final String STATE = "state"; + + /** + * When your application knows which user it is trying to authenticate, it can + * provide this parameter as a hint to the Authentication Server. + * Passing this hint will either pre-fill the email box on the sign-in form or select the proper + * multi-login session, thereby simplifying the login flow. + */ + private static final String LOGIN_HINT = "login_hint"; + + + /** + * If this is provided with the value true, and the authorization request is granted, the authorization will include + * any previous authorizations granted to this user/application combination + * for other scopes; see Incremental Authorization. + */ + private static final String INCLUDE_GRANTED_SCOPES = "include_granted_scopes"; + + // Scopes as defined in http://code.google.com/apis/accounts/docs/OAuth.html#prepScope + private static final String GMAIL_SCOPE = "https://mail.google.com/"; + private static final String CONTACTS_SCOPE = "https://www.google.com/m8/feeds/"; + private static final String DEFAULT_SCOPE = GMAIL_SCOPE + " " + CONTACTS_SCOPE; + + private static final String REDIRECT_OOB = "urn:ietf:wg:oauth:2.0:oob"; + private static final String CONTACTS_URL = "https://www.google.com/m8/feeds/contacts/default/thin?max-results=1"; + + /** + * As defined in the OAuth 2.0 specification, this field must contain a value of authorization_code. + */ + private static final String GRANT_TYPE = "grant_type"; + private static final String AUTHORIZATION_CODE = "authorization_code"; + /** + * The authorization code returned from the initial request. + */ + private static final String CODE = "code"; + private static final String REFRESH_TOKEN = "refresh_token"; + + private final String clientId; + + public OAuth2Client(String clientId) { + if (TextUtils.isEmpty(clientId)) { + throw new IllegalArgumentException("empty client id"); + } + this.clientId = clientId; + } + + public Uri requestUrl() { + return Uri.parse(AUTH_URL) + .buildUpon() + .appendQueryParameter(SCOPE, DEFAULT_SCOPE) + .appendQueryParameter(CLIENT_ID, clientId) + .appendQueryParameter(RESPONSE_TYPE, "code") + .appendQueryParameter(REDIRECT_URI, REDIRECT_OOB).build(); + } + + public OAuth2Token getToken(String code) throws IOException { + HttpsURLConnection connection = postTokenEndpoint(getAccessTokenPostData(code)); + final int responseCode = connection.getResponseCode(); + if (responseCode == HttpsURLConnection.HTTP_OK) { + OAuth2Token token = parseResponse(connection.getInputStream()); + String username = getUsernameFromContacts(token); + Log.d(TAG, "got token " + token+ ", username="+username); + + return new OAuth2Token(token.accessToken, token.tokenType, token.refreshToken, token.expiresIn, username); + } else { + Log.e(TAG, "error: " + responseCode); + throw new IOException("Invalid response from server:" + responseCode); + } + } + + public OAuth2Token refreshToken(String refreshToken) throws IOException { + HttpsURLConnection connection = postTokenEndpoint(getRefreshTokenPostData(refreshToken)); + final int responseCode = connection.getResponseCode(); + if (responseCode == HttpsURLConnection.HTTP_OK) { + return parseResponse(connection.getInputStream()); + } else { + Log.e(TAG, "error: " + responseCode); + throw new IOException("Invalid response from server:" + responseCode); + } + } + + private OAuth2Token parseResponse(InputStream inputStream) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int n; + while ((n = inputStream.read(buffer)) != -1) { + bos.write(buffer, 0, n); + } + inputStream.close(); + return OAuth2Token.fromJSON(bos.toString("UTF-8")); + } + + private HttpsURLConnection postTokenEndpoint(String payload) throws IOException { + HttpsURLConnection connection = (HttpsURLConnection) new URL(TOKEN_URL).openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + final OutputStream os = connection.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); + writer.write(payload); + writer.flush(); + writer.close(); + os.close(); + return connection; + } + + private String getAccessTokenPostData(String code) { + final Uri uri = Uri.parse(TOKEN_URL) + .buildUpon() + .appendQueryParameter(GRANT_TYPE, AUTHORIZATION_CODE) + .appendQueryParameter(REDIRECT_URI, REDIRECT_OOB) + .appendQueryParameter(CLIENT_ID, clientId) + .appendQueryParameter(CODE, code) + .build(); + return uri.getEncodedQuery(); + } + + private String getRefreshTokenPostData(String refreshToken) { + final Uri uri = Uri.parse(TOKEN_URL) + .buildUpon() + .appendQueryParameter(GRANT_TYPE, REFRESH_TOKEN) + .appendQueryParameter(REFRESH_TOKEN, refreshToken) + .appendQueryParameter(CLIENT_ID, clientId) + .build(); + return uri.getEncodedQuery(); + } + + // Retrieves the google email account address using the contacts API + private String getUsernameFromContacts(OAuth2Token token) { + try { + HttpsURLConnection connection = (HttpsURLConnection) new URL(CONTACTS_URL).openConnection(); + connection.addRequestProperty("Authorization", "Bearer "+token.accessToken); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + final InputStream inputStream = connection.getInputStream(); + String email = extractEmail(inputStream); + inputStream.close(); + return email; + } else { + Log.w(TAG, String.format("unexpected server response: %d (%s)", + connection.getResponseCode(), connection.getResponseMessage())); + return null; + } + + } catch (SAXException e) { + Log.e(TAG, "error", e); + return null; + } catch (IOException e) { + Log.e(TAG, "error", e); + return null; + } catch (ParserConfigurationException e) { + Log.e(TAG, "error", e); + return null; + } + } + + private String extractEmail(InputStream inputStream) throws ParserConfigurationException, SAXException, IOException { + final XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); + final FeedHandler feedHandler = new FeedHandler(); + xmlReader.setContentHandler(feedHandler); + xmlReader.parse(new InputSource(inputStream)); + return feedHandler.getEmail(); + } + + public String getClientId() { + return clientId; + } + + private static class FeedHandler extends DefaultHandler { + private static final String EMAIL = "email"; + private static final String AUTHOR = "author"; + private final StringBuilder email = new StringBuilder(); + private boolean inEmail; + private boolean inAuthor; + + @Override + public void startElement(String uri, String localName, String qName, Attributes atts) { + inEmail = EMAIL.equals(qName); + if (AUTHOR.equals(qName)) { + inAuthor = true; + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (inAuthor && AUTHOR.equals(qName)) { + inAuthor = false; + } + } + + @Override + public void characters(char[] c, int start, int length) { + if (inAuthor && inEmail) { + email.append(c, start, length); + } + } + + @Override + public void error(SAXParseException e) throws SAXException { + Log.e(TAG, "error during parsing", e); + } + + @Override public void warning(SAXParseException e) throws SAXException { + Log.w(TAG, "error during parsing", e); + } + + public String getEmail() { + return email.toString().trim(); + } + } +} diff --git a/src/main/java/com/zegoggles/smssync/auth/OAuth2Token.java b/src/main/java/com/zegoggles/smssync/auth/OAuth2Token.java new file mode 100644 index 000000000..c512fbdda --- /dev/null +++ b/src/main/java/com/zegoggles/smssync/auth/OAuth2Token.java @@ -0,0 +1,59 @@ +package com.zegoggles.smssync.auth; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.IOException; + +public class OAuth2Token { + public final String accessToken; + public final String tokenType; + public final String refreshToken; + public final int expiresIn; + public final String userName; + + public OAuth2Token(String accessToken, String tokenType, String refreshToken, int expiresIn, String userName) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.refreshToken = refreshToken; + this.expiresIn = expiresIn; + this.userName = userName; + } + + public static OAuth2Token fromJSON(String string) throws IOException { + try { + Object value = new JSONTokener(string).nextValue(); + if (value instanceof JSONObject) { + return fromJSON((JSONObject) value); + } else { + throw new IOException("Invalid JSON data: "+value); + } + } catch (JSONException e) { + throw new IOException("Error parsing data: "+e.getMessage()); + } + } + + public static OAuth2Token fromJSON(JSONObject object) throws IOException { + try { + return new OAuth2Token( + object.getString("access_token"), + object.getString("token_type"), + object.getString("refresh_token"), + object.getInt("expires_in"), null); + } catch (JSONException e) { + throw new IOException("parse error"); + } + } + + @Override + public String toString() { + return "Token{" + + "accessToken='" + accessToken + '\'' + + ", tokenType='" + tokenType + '\'' + + ", refreshToken='" + refreshToken + '\'' + + ", expiresIn=" + expiresIn + + ", userName='" + userName + '\'' + + '}'; + } +} diff --git a/src/main/java/com/zegoggles/smssync/auth/TokenRefresher.java b/src/main/java/com/zegoggles/smssync/auth/TokenRefresher.java index d3ea5e373..36be5d01f 100644 --- a/src/main/java/com/zegoggles/smssync/auth/TokenRefresher.java +++ b/src/main/java/com/zegoggles/smssync/auth/TokenRefresher.java @@ -8,13 +8,13 @@ import android.content.Context; import android.os.Build; import android.os.Bundle; -import android.text.TextUtils; import android.util.Log; import com.zegoggles.smssync.preferences.AuthPreferences; import org.jetbrains.annotations.Nullable; import java.io.IOException; +import static android.text.TextUtils.isEmpty; import static com.zegoggles.smssync.App.TAG; import static com.zegoggles.smssync.activity.auth.AccountManagerAuthActivity.AUTH_TOKEN_TYPE; import static com.zegoggles.smssync.activity.auth.AccountManagerAuthActivity.GOOGLE_TYPE; @@ -22,64 +22,74 @@ @TargetApi(5) public class TokenRefresher { private @Nullable final AccountManager accountManager; + private final OAuth2Client oauth2Client; private AuthPreferences authPreferences; - - public TokenRefresher(Context context, AuthPreferences authPreferences) { - this(Build.VERSION.SDK_INT >= 5 ? AccountManager.get(context) : null, authPreferences); + public TokenRefresher(Context context, OAuth2Client oauth2Client, AuthPreferences authPreferences) { + this(Build.VERSION.SDK_INT >= 5 ? AccountManager.get(context) : null, oauth2Client, authPreferences); } - TokenRefresher(@Nullable AccountManager accountManager, AuthPreferences authPreferences) { + TokenRefresher(@Nullable AccountManager accountManager, OAuth2Client oauth2Client, + AuthPreferences authPreferences) { this.authPreferences = authPreferences; + this.oauth2Client = oauth2Client; this.accountManager = accountManager; } public void refreshOAuth2Token() throws TokenRefreshException{ - if (accountManager == null) throw new TokenRefreshException("account manager is null"); - final String token = authPreferences.getOauth2Token(); - final String name = authPreferences.getUsername(); + final String refreshToken = authPreferences.getOauth2RefreshToken(); + final String name = authPreferences.getUsername(); - if (!TextUtils.isEmpty(token)) { - invalidateToken(token); - try { - Bundle bundle = accountManager.getAuthToken( - new Account(name, GOOGLE_TYPE), - AUTH_TOKEN_TYPE, - true, /* notify on failure */ - null, /* callback */ - null /* handler */ - ).getResult(); + if (isEmpty(token)) { + throw new TokenRefreshException("no current token set"); + } - if (bundle != null) { - String newToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); + if (!isEmpty(refreshToken)) { + // user authenticated using webflow + refreshUsingOAuth2Client(name, refreshToken); + } else { + refreshUsingAccountManager(token, name); + } + } + + private void refreshUsingAccountManager(String token, String name) throws TokenRefreshException { + if (accountManager == null) throw new TokenRefreshException("account manager is null"); + invalidateToken(token); + try { + Bundle bundle = accountManager.getAuthToken( + new Account(name, GOOGLE_TYPE), + AUTH_TOKEN_TYPE, + true, /* notify on failure */ + null, /* callback */ + null /* handler */ + ).getResult(); - if (!TextUtils.isEmpty(newToken)) { - authPreferences.setOauth2Token(name, newToken); - } else { - throw new TokenRefreshException("no new token obtained"); - } + if (bundle != null) { + String newToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); + + if (!isEmpty(newToken)) { + authPreferences.setOauth2Token(name, newToken, null); } else { - throw new TokenRefreshException("no bundle received from accountmanager"); + throw new TokenRefreshException("no new token obtained"); } - } catch (OperationCanceledException e) { - Log.w(TAG, e); - throw new TokenRefreshException(e); - } catch (IOException e) { - Log.w(TAG, e); - throw new TokenRefreshException(e); - } catch (AuthenticatorException e) { - Log.w(TAG, e); - throw new TokenRefreshException(e); + } else { + throw new TokenRefreshException("no bundle received from accountmanager"); } - } else { - throw new TokenRefreshException("no current token set"); + } catch (OperationCanceledException e) { + Log.w(TAG, e); + throw new TokenRefreshException(e); + } catch (IOException e) { + Log.w(TAG, e); + throw new TokenRefreshException(e); + } catch (AuthenticatorException e) { + Log.w(TAG, e); + throw new TokenRefreshException(e); } } public boolean invalidateToken(String token) { if (accountManager != null) { - // USE_CREDENTIALS permission should be enough according to docs // but some systems require MANAGE_ACCOUNTS @@ -93,4 +103,13 @@ public boolean invalidateToken(String token) { } return false; } + + private void refreshUsingOAuth2Client(String name, String refreshToken) throws TokenRefreshException { + try { + final OAuth2Token token = oauth2Client.refreshToken(refreshToken); + authPreferences.setOauth2Token(name, token.accessToken, isEmpty(token.refreshToken) ? refreshToken : token.refreshToken); + } catch (IOException e) { + throw new TokenRefreshException(e); + } + } } diff --git a/src/main/java/com/zegoggles/smssync/auth/XOAuthConsumer.java b/src/main/java/com/zegoggles/smssync/auth/XOAuthConsumer.java index 4dd0071bf..1b9243440 100644 --- a/src/main/java/com/zegoggles/smssync/auth/XOAuthConsumer.java +++ b/src/main/java/com/zegoggles/smssync/auth/XOAuthConsumer.java @@ -25,10 +25,20 @@ import oauth.signpost.http.HttpRequest; import oauth.signpost.signature.SignatureBaseString; import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -42,18 +52,23 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.SortedSet; import static com.zegoggles.smssync.App.TAG; +import static java.net.HttpURLConnection.HTTP_OK; import static oauth.signpost.OAuth.ENCODING; import static oauth.signpost.OAuth.OAUTH_SIGNATURE; import static oauth.signpost.OAuth.percentEncode; +@Deprecated public class XOAuthConsumer extends CommonsHttpOAuthConsumer { private static final String MAC_NAME = "HmacSHA1"; private static final String ANONYMOUS = "anonymous"; @@ -136,6 +151,52 @@ public CommonsHttpOAuthProvider getProvider(Context context) { }; } + /** + * @param clientId the oauth2 client id + * @return an oauth2 refresh token + * @throws IOException + */ + public String migrateToken(String clientId) throws IOException { + HttpPost post = new HttpPost(OAuth2Client.TOKEN_URL); + List postParams = new ArrayList(); + postParams.add(new BasicNameValuePair("grant_type", "urn:ietf:params:oauth:grant-type:migration:oauth1")); + postParams.add(new BasicNameValuePair("client_id", clientId)); + + try { + post.setEntity(new UrlEncodedFormEntity(postParams)); + } catch (UnsupportedEncodingException e) { + return null; + } + + try { + HttpUriRequest request = (HttpUriRequest) sign(post); + final HttpClient httpClient = new DefaultHttpClient(); + final HttpResponse response = httpClient.execute(request); + + // After a migration request is validated, your application is issued a refresh token + // with down-scoped scopes. The response body is in JSON format, and it contains only an OAuth 2.0 refresh token + // (no access token). + if (response.getStatusLine().getStatusCode() == HTTP_OK) { + final HttpEntity responseString = response.getEntity(); + JSONTokener tokener = new JSONTokener(EntityUtils.toString(responseString)); + Object value = tokener.nextValue(); + + if (value instanceof JSONObject) { + return ((JSONObject)value).getString("refresh_token"); + } else { + Log.w(TAG, "invalid response from server: " + responseString); + } + } else { + Log.w(TAG, "invalid response from server: " + response.getStatusLine()); + } + } catch (OAuthException ignored) { + Log.w(TAG, ignored); + } catch (JSONException e) { + Log.w(TAG, e); + } + return null; + } + private String requestTokenEndpointUrl(Context context) { return String.format(REQUEST_TOKEN_URL, urlEncode(DEFAULT_SCOPE), diff --git a/src/main/java/com/zegoggles/smssync/preferences/AuthPreferences.java b/src/main/java/com/zegoggles/smssync/preferences/AuthPreferences.java index 3040ea2e9..8e65f9580 100644 --- a/src/main/java/com/zegoggles/smssync/preferences/AuthPreferences.java +++ b/src/main/java/com/zegoggles/smssync/preferences/AuthPreferences.java @@ -6,6 +6,8 @@ import android.text.TextUtils; import android.util.Log; import com.fsck.k9.mail.AuthType; +import com.zegoggles.smssync.R; +import com.zegoggles.smssync.auth.OAuth2Client; import com.zegoggles.smssync.auth.TokenRefresher; import com.zegoggles.smssync.auth.XOAuthConsumer; import org.apache.commons.codec.binary.Base64; @@ -46,6 +48,7 @@ public AuthPreferences(Context context) { private static final String OAUTH_USER = "oauth_user"; private static final String OAUTH2_USER = "oauth2_user"; private static final String OAUTH2_TOKEN = "oauth2_token"; + private static final String OAUTH2_REFRESH_TOKEN = "oauth2_refresh_token"; /** * IMAP URI. @@ -72,6 +75,10 @@ public String getOauth2Token() { return getCredentials().getString(OAUTH2_TOKEN, null); } + public String getOauth2RefreshToken() { + return getCredentials().getString(OAUTH2_REFRESH_TOKEN, null); + } + public boolean hasOauthTokens() { return getOauthUsername() != null && getOauthToken() != null && @@ -87,10 +94,12 @@ public String getUsername() { return preferences.getString(OAUTH_USER, getOauth2Username()); } + @Deprecated public void setOauthUsername(String s) { preferences.edit().putString(OAUTH_USER, s).commit(); } + @Deprecated public void setOauthTokens(String token, String secret) { getCredentials().edit() .putString(OAUTH_TOKEN, token) @@ -98,13 +107,29 @@ public void setOauthTokens(String token, String secret) { .commit(); } - public void setOauth2Token(String username, String token) { + @Deprecated + public boolean needsMigration() { + return hasOauthTokens() && !hasOAuth2Tokens(); + } + + public void setOauth2Token(String username, String accessToken, String refreshToken) { preferences.edit() .putString(OAUTH2_USER, username) .commit(); getCredentials().edit() - .putString(OAUTH2_TOKEN, token) + .putString(OAUTH2_TOKEN, accessToken) + .commit(); + getCredentials().edit() + .putString(OAUTH2_REFRESH_TOKEN, refreshToken) + .commit(); + } + + public void clearOAuth1Data() { + preferences.edit().remove(OAUTH_USER).commit(); + getCredentials().edit() + .remove(OAUTH_TOKEN) + .remove(OAUTH_TOKEN_SECRET) .commit(); } @@ -120,13 +145,17 @@ public void clearOauthData() { .remove(OAUTH_TOKEN) .remove(OAUTH_TOKEN_SECRET) .remove(OAUTH2_TOKEN) + .remove(OAUTH2_REFRESH_TOKEN) .commit(); if (!TextUtils.isEmpty(oauth2token)) { - new TokenRefresher(context, this).invalidateToken(oauth2token); + new TokenRefresher(context, new OAuth2Client(getOAuth2ClientId()), this).invalidateToken(oauth2token); } } + public String getOAuth2ClientId() { + return context.getString(R.string.oauth2_client_id); + } public void setImapPassword(String s) { getCredentials().edit().putString(LOGIN_PASSWORD, s).commit(); diff --git a/src/main/java/com/zegoggles/smssync/service/BackupTask.java b/src/main/java/com/zegoggles/smssync/service/BackupTask.java index 80471d441..39b325645 100644 --- a/src/main/java/com/zegoggles/smssync/service/BackupTask.java +++ b/src/main/java/com/zegoggles/smssync/service/BackupTask.java @@ -10,6 +10,7 @@ import com.squareup.otto.Subscribe; import com.zegoggles.smssync.App; import com.zegoggles.smssync.R; +import com.zegoggles.smssync.auth.OAuth2Client; import com.zegoggles.smssync.auth.TokenRefreshException; import com.zegoggles.smssync.auth.TokenRefresher; import com.zegoggles.smssync.calendar.CalendarAccessor; @@ -80,7 +81,7 @@ class BackupTask extends AsyncTask { } else { calendarSyncer = null; } - this.tokenRefresher = new TokenRefresher(service, authPreferences); + this.tokenRefresher = new TokenRefresher(service, new OAuth2Client(authPreferences.getOAuth2ClientId()), authPreferences); } BackupTask(SmsBackupService service, diff --git a/src/main/java/com/zegoggles/smssync/service/SmsBackupService.java b/src/main/java/com/zegoggles/smssync/service/SmsBackupService.java index d2e6b66ac..dad21143f 100644 --- a/src/main/java/com/zegoggles/smssync/service/SmsBackupService.java +++ b/src/main/java/com/zegoggles/smssync/service/SmsBackupService.java @@ -27,6 +27,7 @@ import com.zegoggles.smssync.App; import com.zegoggles.smssync.Consts; import com.zegoggles.smssync.R; +import com.zegoggles.smssync.auth.OAuth2Client; import com.zegoggles.smssync.mail.BackupImapStore; import com.zegoggles.smssync.mail.DataType; import com.zegoggles.smssync.service.exception.BackupDisabledException; @@ -37,6 +38,7 @@ import com.zegoggles.smssync.service.exception.RequiresWifiException; import com.zegoggles.smssync.service.state.BackupState; import com.zegoggles.smssync.service.state.SmsSyncState; +import com.zegoggles.smssync.tasks.MigrateOAuth1TokenTask; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -81,10 +83,13 @@ protected void handleIntent(final Intent intent) { ", type="+backupType+")"); appLog(R.string.app_log_backup_requested, getString(backupType.resId)); - // Only start a backup if there's no other operation going on at this time. if (!isWorking() && !SmsRestoreService.isServiceWorking()) { - backup(backupType, intent.getBooleanExtra(Consts.KEY_SKIP_MESSAGES, false)); + if (getAuthPreferences().needsMigration()) { + runMigration(); + } else { + backup(backupType, intent.getBooleanExtra(Consts.KEY_SKIP_MESSAGES, false)); + } } else { appLog(R.string.app_log_skip_backup_already_running); } @@ -282,4 +287,14 @@ public static boolean isServiceWorking() { public BackupState transition(SmsSyncState newState, Exception e) { return mState.transition(newState, e); } + + @Deprecated + private void runMigration() { + final MigrateOAuth1TokenTask migrationTask = new MigrateOAuth1TokenTask( + getAuthPreferences().getOAuthConsumer(), + new OAuth2Client(getAuthPreferences().getOAuth2ClientId()), + getAuthPreferences()); + + migrationTask.execute(); + } } diff --git a/src/main/java/com/zegoggles/smssync/service/SmsRestoreService.java b/src/main/java/com/zegoggles/smssync/service/SmsRestoreService.java index c820da9dd..b08289a03 100644 --- a/src/main/java/com/zegoggles/smssync/service/SmsRestoreService.java +++ b/src/main/java/com/zegoggles/smssync/service/SmsRestoreService.java @@ -12,6 +12,7 @@ import com.squareup.otto.Subscribe; import com.zegoggles.smssync.App; import com.zegoggles.smssync.R; +import com.zegoggles.smssync.auth.OAuth2Client; import com.zegoggles.smssync.auth.TokenRefresher; import com.zegoggles.smssync.contacts.ContactAccessor; import com.zegoggles.smssync.mail.MessageConverter; @@ -97,8 +98,9 @@ protected void handleIntent(final Intent intent) { 0 ); + final AuthPreferences authPreferences = new AuthPreferences(this); new RestoreTask(this, converter, getContentResolver(), - new TokenRefresher(service, new AuthPreferences(this))).execute(config); + new TokenRefresher(service, new OAuth2Client(authPreferences.getOAuth2ClientId()), authPreferences)).execute(config); } catch (MessagingException e) { postError(e); diff --git a/src/main/java/com/zegoggles/smssync/tasks/MigrateOAuth1TokenTask.java b/src/main/java/com/zegoggles/smssync/tasks/MigrateOAuth1TokenTask.java new file mode 100644 index 000000000..b8477f988 --- /dev/null +++ b/src/main/java/com/zegoggles/smssync/tasks/MigrateOAuth1TokenTask.java @@ -0,0 +1,54 @@ +package com.zegoggles.smssync.tasks; + +import android.os.AsyncTask; +import android.text.TextUtils; +import android.util.Log; +import com.zegoggles.smssync.auth.OAuth2Client; +import com.zegoggles.smssync.auth.OAuth2Token; +import com.zegoggles.smssync.auth.XOAuthConsumer; +import com.zegoggles.smssync.preferences.AuthPreferences; + +import java.io.IOException; + +import static com.zegoggles.smssync.App.TAG; + +public class MigrateOAuth1TokenTask extends AsyncTask { + private final XOAuthConsumer xoauthConsumer; + private final OAuth2Client oAuth2Client; + private final AuthPreferences authPreferences; + + public MigrateOAuth1TokenTask(XOAuthConsumer xoauthConsumer, + OAuth2Client oauth2Client, + AuthPreferences authPreferences) { + this.xoauthConsumer = xoauthConsumer; + this.oAuth2Client = oauth2Client; + this.authPreferences = authPreferences; + } + + @Override + protected OAuth2Token doInBackground(Void... empty) { + try { + String refreshToken = xoauthConsumer.migrateToken(oAuth2Client.getClientId()); + if (!TextUtils.isEmpty(refreshToken)) { + return oAuth2Client.refreshToken(refreshToken); + } else { + Log.w(TAG, "did not get a refresh token"); + } + } catch (IOException e) { + Log.w(TAG, e); + } + return null; + } + + @Override + protected void onPostExecute(OAuth2Token oAuth2Token) { + if (oAuth2Token != null) { + authPreferences.setOauth2Token( + authPreferences.getUsername(), + oAuth2Token.accessToken, + oAuth2Token.refreshToken); + + authPreferences.clearOAuth1Data(); + } + } +} diff --git a/src/main/java/com/zegoggles/smssync/tasks/OAuth2CallbackTask.java b/src/main/java/com/zegoggles/smssync/tasks/OAuth2CallbackTask.java new file mode 100644 index 000000000..4dca8de55 --- /dev/null +++ b/src/main/java/com/zegoggles/smssync/tasks/OAuth2CallbackTask.java @@ -0,0 +1,53 @@ +package com.zegoggles.smssync.tasks; + +import android.os.AsyncTask; +import android.text.TextUtils; +import android.util.Log; +import com.zegoggles.smssync.App; +import com.zegoggles.smssync.auth.OAuth2Client; +import com.zegoggles.smssync.auth.OAuth2Token; + +import java.io.IOException; +import java.util.Arrays; + +import static com.zegoggles.smssync.App.TAG; + +public class OAuth2CallbackTask extends AsyncTask { + + private final OAuth2Client oauth2Client; + + public OAuth2CallbackTask(OAuth2Client oauth2Client) { + this.oauth2Client = oauth2Client; + } + + @Override + protected OAuth2Token doInBackground(String... code) { + if (code == null || code.length == 0 || TextUtils.isEmpty(code[0])) { + Log.w(TAG, "invalid input: "+ Arrays.toString(code)); + return null; + } + try { + return oauth2Client.getToken(code[0]); + } catch (IOException e) { + Log.w(TAG, e); + } + return null; + } + + @Override + protected void onPostExecute(OAuth2Token token) { + App.bus.post(new OAuth2CallbackEvent(token)); + } + + public static class OAuth2CallbackEvent { + public final OAuth2Token token; + + public OAuth2CallbackEvent(OAuth2Token token) { + this.token = token; + } + + public boolean valid() { + return token != null; + } + } +} diff --git a/src/main/java/com/zegoggles/smssync/tasks/OAuthCallbackTask.java b/src/main/java/com/zegoggles/smssync/tasks/OAuthCallbackTask.java deleted file mode 100644 index 0e7ece42d..000000000 --- a/src/main/java/com/zegoggles/smssync/tasks/OAuthCallbackTask.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.zegoggles.smssync.tasks; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.AsyncTask; -import android.text.TextUtils; -import android.util.Log; -import com.zegoggles.smssync.App; -import com.zegoggles.smssync.auth.XOAuthConsumer; -import com.zegoggles.smssync.preferences.AuthPreferences; -import oauth.signpost.OAuth; -import oauth.signpost.commonshttp.CommonsHttpOAuthProvider; -import oauth.signpost.exception.OAuthException; - -import static com.zegoggles.smssync.App.LOCAL_LOGV; -import static com.zegoggles.smssync.App.TAG; - -public class OAuthCallbackTask extends AsyncTask { - private Context context; - - public OAuthCallbackTask(Context smsSync) { - this.context = smsSync; - } - - @Override - protected XOAuthConsumer doInBackground(Intent... callbackIntent) { - Uri uri = callbackIntent[0].getData(); - if (LOCAL_LOGV) Log.v(TAG, "oauth callback: " + uri); - - XOAuthConsumer consumer = new AuthPreferences(context).getOAuthConsumer(); - CommonsHttpOAuthProvider provider = consumer.getProvider(context); - String verifier = uri.getQueryParameter(OAuth.OAUTH_VERIFIER); - try { - provider.retrieveAccessToken(consumer, verifier); - String username = consumer.loadUsernameFromContacts(); - - if (username != null) { - Log.i(TAG, "Valid access token for " + username); - // intent has been handled - callbackIntent[0].setData(null); - - return consumer; - } else { - Log.e(TAG, "No valid user name"); - return null; - } - } catch (OAuthException e) { - Log.e(TAG, "error", e); - return null; - } - } - - @Override - protected void onPostExecute(XOAuthConsumer consumer) { - if (LOCAL_LOGV) - Log.v(TAG, String.format("%s#onPostExecute(%s)", getClass().getName(), consumer)); - - if (consumer != null) { - App.bus.post(new OAuthCallbackEvent(consumer.getUsername(), - consumer.getToken(), - consumer.getTokenSecret())); - } else { - App.bus.post(new OAuthCallbackEvent(null, null, null)); - } - } - - public static class OAuthCallbackEvent { - public final String username, token, tokenSecret; - - public OAuthCallbackEvent(String username, String token, String tokenSecret) { - this.username = username; - this.token = token; - this.tokenSecret = tokenSecret; - } - - public boolean valid() { - return !TextUtils.isEmpty(username) && - !TextUtils.isEmpty(token) && - !TextUtils.isEmpty(tokenSecret); - } - } -} diff --git a/src/main/java/com/zegoggles/smssync/tasks/RequestTokenTask.java b/src/main/java/com/zegoggles/smssync/tasks/RequestTokenTask.java deleted file mode 100644 index ebfb388ac..000000000 --- a/src/main/java/com/zegoggles/smssync/tasks/RequestTokenTask.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.zegoggles.smssync.tasks; - -import android.content.Context; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.Log; -import com.zegoggles.smssync.App; -import com.zegoggles.smssync.auth.XOAuthConsumer; -import com.zegoggles.smssync.preferences.AuthPreferences; -import oauth.signpost.commonshttp.CommonsHttpOAuthProvider; -import oauth.signpost.exception.OAuthCommunicationException; -import oauth.signpost.exception.OAuthException; - -import static com.zegoggles.smssync.App.TAG; - -public class RequestTokenTask extends AsyncTask { - private Context context; - private AuthPreferences authPreferences; - - public RequestTokenTask(Context context) { - this.context = context; - this.authPreferences = new AuthPreferences(context); - } - - public String doInBackground(String... callback) { - synchronized (XOAuthConsumer.class) { - XOAuthConsumer consumer = authPreferences.getOAuthConsumer(); - CommonsHttpOAuthProvider provider = consumer.getProvider(context); - try { - String url = provider.retrieveRequestToken(consumer, callback[0]); - authPreferences.setOauthTokens(consumer.getToken(), consumer.getTokenSecret()); - return url; - } catch (OAuthCommunicationException e) { - Log.e(TAG, "error requesting token: " + e.getResponseBody(), e); - return null; - } catch (OAuthException e) { - Log.e(TAG, "error requesting token", e); - return null; - } - } - } - - @Override - protected void onPostExecute(String authorizeUrl) { - if (authorizeUrl != null) { - App.bus.post(new AuthorizedURLReceived(Uri.parse(authorizeUrl))); - } else { - App.bus.post(new AuthorizedURLReceived(null)); - } - } - - public static class AuthorizedURLReceived { - public final Uri uri; - - public AuthorizedURLReceived(Uri uri) { - this.uri = uri; - } - } -} diff --git a/src/test/java/com/zegoggles/smssync/auth/TokenRefresherTest.java b/src/test/java/com/zegoggles/smssync/auth/TokenRefresherTest.java index 8206aad56..65ab8620d 100644 --- a/src/test/java/com/zegoggles/smssync/auth/TokenRefresherTest.java +++ b/src/test/java/com/zegoggles/smssync/auth/TokenRefresherTest.java @@ -26,11 +26,12 @@ public class TokenRefresherTest { @Mock AccountManager accountManager; @Mock AuthPreferences authPreferences; + @Mock OAuth2Client oauth2Client; TokenRefresher refresher; @Before public void before() { initMocks(this); - refresher = new TokenRefresher(accountManager, authPreferences); + refresher = new TokenRefresher(accountManager, oauth2Client, authPreferences); } @Test public void shouldInvalidateTokenManually() throws Exception { @@ -106,6 +107,30 @@ public class TokenRefresherTest { refresher.refreshOAuth2Token(); - verify(authPreferences).setOauth2Token("username", "newToken"); + verify(authPreferences).setOauth2Token("username", "newToken", null); + } + + @Test public void shouldUseOAuth2ClientWhenRefreshTokenIsPresent() throws Exception { + when(authPreferences.getOauth2Token()).thenReturn("token"); + when(authPreferences.getOauth2RefreshToken()).thenReturn("refresh"); + when(authPreferences.getUsername()).thenReturn("username"); + + when(oauth2Client.refreshToken("refresh")).thenReturn(new OAuth2Token("newToken", "type", null, 0, null)); + + refresher.refreshOAuth2Token(); + + verify(authPreferences).setOauth2Token("username", "newToken", "refresh"); + } + + @Test public void shouldUpdateRefreshTokenIfPresentInResponse() throws Exception { + when(authPreferences.getOauth2Token()).thenReturn("token"); + when(authPreferences.getOauth2RefreshToken()).thenReturn("refresh"); + when(authPreferences.getUsername()).thenReturn("username"); + + when(oauth2Client.refreshToken("refresh")).thenReturn(new OAuth2Token("newToken", "type", "newRefresh", 0, null)); + + refresher.refreshOAuth2Token(); + + verify(authPreferences).setOauth2Token("username", "newToken", "newRefresh"); } } diff --git a/src/test/java/com/zegoggles/smssync/preferences/AuthPreferencesTest.java b/src/test/java/com/zegoggles/smssync/preferences/AuthPreferencesTest.java index 5d00a6d15..141ddd10b 100644 --- a/src/test/java/com/zegoggles/smssync/preferences/AuthPreferencesTest.java +++ b/src/test/java/com/zegoggles/smssync/preferences/AuthPreferencesTest.java @@ -35,7 +35,7 @@ public class AuthPreferencesTest { when(serverPreferences.getServerAddress()).thenReturn(ServerPreferences.Defaults.SERVER_ADDRESS); when(serverPreferences.isGmail()).thenReturn(true); - authPreferences.setOauth2Token("user", "token"); + authPreferences.setOauth2Token("user", "token", null); authPreferences.setServerAuthMode(AuthType.XOAUTH2); assertThat(authPreferences.getStoreUri()).isEqualTo("imap+ssl+://XOAUTH2:user:dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE%253D@imap.gmail.com:993");