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");