package cgeo.geocaching.activity; import cgeo.geocaching.Intents; import cgeo.geocaching.R; import cgeo.geocaching.network.Network; import cgeo.geocaching.network.OAuth; import cgeo.geocaching.network.OAuthTokens; import cgeo.geocaching.network.Parameters; import cgeo.geocaching.ui.WeakReferenceHandler; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.AndroidRxUtils; import cgeo.geocaching.utils.BundleUtils; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.MatcherWrapper; import android.app.Activity; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.View; import android.widget.Button; import android.widget.TextView; import java.util.regex.Pattern; import butterknife.BindView; import okhttp3.HttpUrl; import okhttp3.Response; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; public abstract class OAuthAuthorizationActivity extends AbstractActivity { public static final int NOT_AUTHENTICATED = 0; public static final int AUTHENTICATED = 1; private static final int STATUS_ERROR = 0; private static final int STATUS_SUCCESS = 1; private static final int STATUS_ERROR_EXT_MSG = 2; private static final Pattern PARAMS_PATTERN_1 = Pattern.compile("oauth_token=([\\w_.-]+)"); private static final Pattern PARAMS_PATTERN_2 = Pattern.compile("oauth_token_secret=([\\w_.-]+)"); @NonNull private String host = StringUtils.EMPTY; @NonNull private String pathRequest = StringUtils.EMPTY; @NonNull private String pathAuthorize = StringUtils.EMPTY; @NonNull private String pathAccess = StringUtils.EMPTY; private boolean https = false; @NonNull private String consumerKey = StringUtils.EMPTY; @NonNull private String consumerSecret = StringUtils.EMPTY; @NonNull private String callback = StringUtils.EMPTY; private String oAtoken = null; private String oAtokenSecret = null; @BindView(R.id.start) protected Button startButton; @BindView(R.id.register) protected Button registerButton; @BindView(R.id.auth_1) protected TextView auth1; @BindView(R.id.auth_2) protected TextView auth2; private ProgressDialog requestTokenDialog = null; private ProgressDialog changeTokensDialog = null; private final Handler requestTokenHandler = new RequestTokenHandler(this); private final Handler changeTokensHandler = new ChangeTokensHandler(this); private static final class RequestTokenHandler extends WeakReferenceHandler<OAuthAuthorizationActivity> { RequestTokenHandler(final OAuthAuthorizationActivity activity) { super(activity); } @Override public void handleMessage(final Message msg) { final OAuthAuthorizationActivity activity = getReference(); if (activity != null) { final ProgressDialog requestTokenDialog = activity.requestTokenDialog; if (requestTokenDialog != null && requestTokenDialog.isShowing()) { requestTokenDialog.dismiss(); } final Button startButton = activity.startButton; startButton.setOnClickListener(new StartListener(activity)); startButton.setEnabled(true); if (msg.what == STATUS_SUCCESS) { startButton.setText(activity.getAuthAgain()); } else if (msg.what == STATUS_ERROR_EXT_MSG) { String errMsg = activity.getErrAuthInitialize(); errMsg += msg.obj != null ? "\n" + msg.obj.toString() : ""; activity.showToast(errMsg); startButton.setText(activity.getAuthStart()); } else { activity.showToast(activity.getErrAuthInitialize()); startButton.setText(activity.getAuthStart()); } } } } private static final class ChangeTokensHandler extends WeakReferenceHandler<OAuthAuthorizationActivity> { ChangeTokensHandler(final OAuthAuthorizationActivity activity) { super(activity); } @Override public void handleMessage(final Message msg) { final OAuthAuthorizationActivity activity = getReference(); if (activity != null) { Dialogs.dismiss(activity.changeTokensDialog); if (msg.what == AUTHENTICATED) { activity.showToast(activity.getAuthDialogCompleted()); activity.setResult(RESULT_OK); activity.finish(); } else { activity.showToast(activity.getErrAuthProcess()); activity.startButton.setText(activity.getAuthStart()); } } } } @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState, R.layout.authorization_activity); final Bundle extras = getIntent().getExtras(); if (extras != null) { host = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_HOST, host); pathRequest = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_PATH_REQUEST, pathRequest); pathAuthorize = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_PATH_AUTHORIZE, pathAuthorize); pathAccess = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_PATH_ACCESS, pathAccess); https = extras.getBoolean(Intents.EXTRA_OAUTH_HTTPS, https); consumerKey = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_CONSUMER_KEY, consumerKey); consumerSecret = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_CONSUMER_SECRET, consumerSecret); callback = BundleUtils.getString(extras, Intents.EXTRA_OAUTH_CALLBACK, callback); } setTitle(getAuthTitle()); auth1.setText(getAuthExplainShort()); auth2.setText(getAuthExplainLong()); final ImmutablePair<String, String> tempToken = getTempTokens(); oAtoken = tempToken.left; oAtokenSecret = tempToken.right; startButton.setText(getAuthAuthorize()); startButton.setEnabled(true); startButton.setOnClickListener(new StartListener(this)); if (StringUtils.isEmpty(getCreateAccountUrl())) { registerButton.setVisibility(View.GONE); } else { registerButton.setText(getAuthRegister()); registerButton.setEnabled(true); registerButton.setOnClickListener(new RegisterListener()); } if (StringUtils.isBlank(oAtoken) && StringUtils.isBlank(oAtokenSecret)) { // start authorization process startButton.setText(getAuthStart()); } else { // already have temporary tokens, continue from pin startButton.setText(getAuthAgain()); } } @Override public void onNewIntent(final Intent intent) { setIntent(intent); } @Override public void onResume() { super.onResume(); final Uri uri = getIntent().getData(); if (uri != null) { final String verifier = uri.getQueryParameter("oauth_verifier"); if (StringUtils.isNotBlank(verifier)) { exchangeTokens(verifier); } else { // We can shortcut the whole verification process if we do not have a token at all. changeTokensHandler.sendEmptyMessage(NOT_AUTHENTICATED); } } } private void requestToken() { final Parameters params = new Parameters(); params.put("oauth_callback", callback); final String method = "GET"; OAuth.signOAuth(host, pathRequest, method, https, params, new OAuthTokens(null, null), consumerKey, consumerSecret); try { final Response response = Network.getRequest(getUrlPrefix() + host + pathRequest, params).blockingGet(); if (response.isSuccessful()) { final String line = Network.getResponseData(response); int status = STATUS_ERROR; if (StringUtils.isNotBlank(line)) { assert line != null; final MatcherWrapper paramsMatcher1 = new MatcherWrapper(PARAMS_PATTERN_1, line); if (paramsMatcher1.find()) { oAtoken = paramsMatcher1.group(1); } final MatcherWrapper paramsMatcher2 = new MatcherWrapper(PARAMS_PATTERN_2, line); if (paramsMatcher2.find()) { oAtokenSecret = paramsMatcher2.group(1); } if (StringUtils.isNotBlank(oAtoken) && StringUtils.isNotBlank(oAtokenSecret)) { setTempTokens(oAtoken, oAtokenSecret); final HttpUrl url = HttpUrl.parse(getUrlPrefix() + host + pathAuthorize) .newBuilder().addQueryParameter("oauth_token", oAtoken).build(); startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString()))); status = STATUS_SUCCESS; } } requestTokenHandler.sendEmptyMessage(status); } else { final String extErrMsg = getExtendedErrorMsg(response); if (StringUtils.isNotBlank(extErrMsg)) { final Message msg = requestTokenHandler.obtainMessage(STATUS_ERROR_EXT_MSG, extErrMsg); requestTokenHandler.sendMessage(msg); } else { requestTokenHandler.sendEmptyMessage(STATUS_ERROR); } } } catch (final RuntimeException ignored) { Log.e("requestToken: cannot get token"); requestTokenHandler.sendEmptyMessage(STATUS_ERROR); } } private void changeToken(final String verifier) { int status = NOT_AUTHENTICATED; try { final Parameters params = new Parameters("oauth_verifier", verifier); final String method = "POST"; OAuth.signOAuth(host, pathAccess, method, https, params, new OAuthTokens(oAtoken, oAtokenSecret), consumerKey, consumerSecret); final String line = StringUtils.defaultString(Network.getResponseData(Network.postRequest(getUrlPrefix() + host + pathAccess, params))); oAtoken = ""; oAtokenSecret = ""; final MatcherWrapper paramsMatcher1 = new MatcherWrapper(PARAMS_PATTERN_1, line); if (paramsMatcher1.find()) { oAtoken = paramsMatcher1.group(1); } final MatcherWrapper paramsMatcher2 = new MatcherWrapper(PARAMS_PATTERN_2, line); if (paramsMatcher2.find()) { oAtokenSecret = paramsMatcher2.group(1); } if (StringUtils.isBlank(oAtoken) && StringUtils.isBlank(oAtokenSecret)) { oAtoken = ""; oAtokenSecret = ""; setTokens(null, null, false); } else { setTokens(oAtoken, oAtokenSecret, true); status = AUTHENTICATED; } } catch (final Exception e) { Log.e("OAuthAuthorizationActivity.changeToken", e); } changeTokensHandler.sendEmptyMessage(status); } private String getUrlPrefix() { return https ? "https://" : "http://"; } private static final class StartListener extends WeakReferenceHandler<OAuthAuthorizationActivity> implements View.OnClickListener { StartListener(final OAuthAuthorizationActivity actitity) { super(actitity); } @Override public void onClick(final View arg0) { final OAuthAuthorizationActivity actitity = getReference(); if (actitity != null) { if (actitity.requestTokenDialog == null) { actitity.requestTokenDialog = new ProgressDialog(actitity); actitity.requestTokenDialog.setCancelable(false); actitity.requestTokenDialog.setMessage(actitity.getAuthDialogWait()); } actitity.requestTokenDialog.show(); final Button startButton = actitity.startButton; startButton.setEnabled(false); startButton.setOnTouchListener(null); startButton.setOnClickListener(null); actitity.setTempTokens(null, null); AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { actitity.requestToken(); } }); } } } private class RegisterListener implements View.OnClickListener { @Override public void onClick(final View view) { final Activity activity = OAuthAuthorizationActivity.this; try { activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getCreateAccountUrl()))); } catch (final ActivityNotFoundException e) { Log.e("Cannot find suitable activity", e); ActivityMixin.showToast(activity, R.string.err_application_no); } } } private void exchangeTokens(final String verifier) { if (changeTokensDialog == null) { changeTokensDialog = new ProgressDialog(this); changeTokensDialog.setCancelable(false); changeTokensDialog.setMessage(getAuthDialogWait()); } changeTokensDialog.show(); AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { changeToken(verifier); } }); } @NonNull protected abstract ImmutablePair<String, String> getTempTokens(); protected abstract void setTempTokens(@Nullable String tokenPublic, @Nullable String tokenSecret); protected abstract void setTokens(@Nullable String tokenPublic, @Nullable String tokenSecret, boolean enable); // get resources from derived class protected abstract String getCreateAccountUrl(); @NonNull protected abstract String getAuthTitle(); private String getAuthAgain() { return getString(R.string.auth_again); } private String getErrAuthInitialize() { return getString(R.string.err_auth_initialize); } private String getAuthStart() { return getString(R.string.auth_start); } protected abstract String getAuthDialogCompleted(); private String getErrAuthProcess() { return res.getString(R.string.err_auth_process); } /** * Allows deriving classes to check the response for error messages specific to their OAuth implementation * * @param response * The error response of the token request * @return String with a more detailed error message (user-facing, localized), can be empty */ protected String getExtendedErrorMsg(final Response response) { return StringUtils.EMPTY; } private String getAuthDialogWait() { return res.getString(R.string.auth_dialog_waiting, getAuthTitle()); } private String getAuthExplainShort() { return res.getString(R.string.auth_explain_short, getAuthTitle()); } private String getAuthExplainLong() { return res.getString(R.string.auth_explain_long, getAuthTitle()); } private String getAuthAuthorize() { return res.getString(R.string.auth_authorize); } protected String getAuthRegister() { return res.getString(R.string.auth_register); } public static class OAuthParameters { @NonNull public final String host; @NonNull public final String pathRequest; @NonNull public final String pathAuthorize; @NonNull public final String pathAccess; public final boolean https; @NonNull public final String consumerKey; @NonNull public final String consumerSecret; @NonNull public final String callback; public OAuthParameters(@NonNull final String host, @NonNull final String pathRequest, @NonNull final String pathAuthorize, @NonNull final String pathAccess, final boolean https, @NonNull final String consumerKey, @NonNull final String consumerSecret, @NonNull final String callback) { this.host = host; this.pathRequest = pathRequest; this.pathAuthorize = pathAuthorize; this.pathAccess = pathAccess; this.https = https; this.consumerKey = consumerKey; this.consumerSecret = consumerSecret; this.callback = callback; } public void setOAuthExtras(final Intent intent) { if (intent != null) { intent.putExtra(Intents.EXTRA_OAUTH_HOST, host); intent.putExtra(Intents.EXTRA_OAUTH_PATH_REQUEST, pathRequest); intent.putExtra(Intents.EXTRA_OAUTH_PATH_AUTHORIZE, pathAuthorize); intent.putExtra(Intents.EXTRA_OAUTH_PATH_ACCESS, pathAccess); intent.putExtra(Intents.EXTRA_OAUTH_HTTPS, https); intent.putExtra(Intents.EXTRA_OAUTH_CONSUMER_KEY, consumerKey); intent.putExtra(Intents.EXTRA_OAUTH_CONSUMER_SECRET, consumerSecret); intent.putExtra(Intents.EXTRA_OAUTH_CALLBACK, callback); } } } @Override public void finish() { Dialogs.dismiss(requestTokenDialog); Dialogs.dismiss(changeTokensDialog); super.finish(); } }