package de.westnordost.streetcomplete.oauth;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.WebView;
import android.widget.Button;
import java.util.List;
import de.westnordost.osmapi.OsmConnection;
import de.westnordost.osmapi.user.PermissionsDao;
import de.westnordost.streetcomplete.R;
import de.westnordost.streetcomplete.data.OsmModule;
import de.westnordost.streetcomplete.util.AsyncTaskListener;
import de.westnordost.streetcomplete.util.InlineAsyncTask;
import oauth.signpost.OAuthConsumer;
import oauth.signpost.OAuthProvider;
import oauth.signpost.exception.OAuthException;
/** Dialog used to authorize the application with OAuth 1.0a. Create this dialog via
* OAuthWebViewDialogFragment.create and provide an OAuthConsumer and OAuthProvider.
*
* The calling class must implement the interface OAuthWebViewDialogFragment.OAuthListener with
* which the class is notified when the process is complete. Check getToken() and getTokenSecret()
* of the parameter passed to onOAuthAuthorized to retrieve the access token and secret, the result
* of the whole authorization process.
*/
public class OAuthWebViewDialogFragment extends DialogFragment
{
public static final String TAG = "OAuth";
// magic callback url for webpage to tell this dialog that the user has confirmed the authorization
private static final String CALLBACK_URL = "streetcomplete://oauth/";
// for loading and saving from bundle
private static final String
CONSUMER = "consumer",
PROVIDER = "provider",
AUTHORIZE_URL = "authorizeURL",
VERIFICATION_CODE = "verificationCode";
// OAuth stuff
private OAuthConsumer consumer;
private OAuthProvider provider;
private String authorizeURL;
private String verificationCode;
// for reporting back with the result
private OAuthListener callbackListener;
// UI components
private WebView webView;
private ViewGroup progressGroup;
private ViewGroup errorGroup;
/** To be implemented by the user of this dialog fragment. Used to report back with the result */
public interface OAuthListener
{
/** Called when the authorization process has successfully finished.
* @param consumer The consumer has the correct access token and secret set.
* @param permissions The permissions granted to the consumer. See constants in
* de.westnordost.osmapi.user.Permission */
void onOAuthAuthorized(OAuthConsumer consumer, List<String> permissions);
/** Called when the authorization process failed because the user closed the dialog */
void onOAuthCancelled();
}
public static OAuthWebViewDialogFragment create(@NonNull OAuthConsumer consumer,
@NonNull OAuthProvider provider)
{
OAuthWebViewDialogFragment f = new OAuthWebViewDialogFragment();
Bundle args = new Bundle();
args.putSerializable(CONSUMER, consumer);
args.putSerializable(PROVIDER, provider);
f.setArguments(args);
return f;
}
/* --------- The below lifecycle methods are ordered in the order they are called ----------- */
@Override public void onAttach(Activity activity)
{
super.onAttach(activity);
try
{
callbackListener = (OAuthListener) activity;
}
catch (ClassCastException e)
{
throw new ClassCastException(activity.toString() + " must implement OAuthListener");
}
}
@Override public void onCreate(Bundle inState)
{
super.onCreate(inState);
// restore...
if(inState != null)
{
consumer = (OAuthConsumer) inState.getSerializable(CONSUMER);
provider = (OAuthProvider) inState.getSerializable(PROVIDER);
authorizeURL = inState.getString(AUTHORIZE_URL);
verificationCode = inState.getString(VERIFICATION_CODE);
}
// or initialize...
else
{
consumer = (OAuthConsumer) getArguments().getSerializable(CONSUMER);
provider = (OAuthProvider) getArguments().getSerializable(PROVIDER);
}
}
@SuppressLint("SetJavaScriptEnabled")
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.oauth_web_view_dialog_fragment, container, false);
getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
progressGroup = (ViewGroup) view.findViewById(R.id.progress);
errorGroup = (ViewGroup) view.findViewById(R.id.error);
webView = (WebView) view.findViewById(R.id.webview);
// Javascript is necessary to display OpenStreetMap's OAuth page correctly
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient( new OAuthCallbackWebViewClient(
new RetrieveVerificationCodeListener(), webView, progressGroup));
Button tryAgain = (Button) view.findViewById(R.id.retry_button);
tryAgain.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
restartAuthentication();
}
});
return view;
}
@Override public void onStart()
{
super.onStart();
continueAuthentication();
}
@Override public void onStop()
{
super.onStop();
webView.stopLoading();
}
@Override public void onSaveInstanceState(@NonNull Bundle outState)
{
outState.putSerializable(CONSUMER, consumer);
outState.putSerializable(PROVIDER, provider);
outState.putString(AUTHORIZE_URL, authorizeURL);
outState.putString(VERIFICATION_CODE, verificationCode);
super.onSaveInstanceState(outState);
}
@Override public void onDestroyView()
{
super.onDestroyView();
webView.destroy();
webView = null;
}
@Override public void onCancel(DialogInterface dialog)
{
super.onCancel(dialog);
if(verificationCode == null)
{
callbackListener.onOAuthCancelled();
}
}
/* ------------------------------------------------------------------------------------------ */
private void restartAuthentication()
{
authorizeURL = null;
verificationCode = null;
continueAuthentication();
}
private void continueAuthentication()
{
if(webView == null) return;
progressGroup.setVisibility(View.VISIBLE);
errorGroup.setVisibility(View.INVISIBLE);
webView.setVisibility(View.INVISIBLE);
if(authorizeURL == null)
{
Log.i(TAG, "Step 1: Retrieving request token...");
new RetrieveRequestTokenTask().execute();
}
else if(verificationCode == null)
{
Log.i(TAG, "Step 2: Authorize app on web page...");
if(!authorizeURL.equals(webView.getOriginalUrl()))
{
webView.loadUrl(authorizeURL);
}
else
{
progressGroup.setVisibility(View.INVISIBLE);
webView.setVisibility(View.VISIBLE);
}
}
else
{
Log.i(TAG, "Step 3: Retrieving access token and getting permissions...");
new RetrieveAccessTokenTask().execute();
}
}
private void finishAuthentication(List<String> permissions)
{
callbackListener.onOAuthAuthorized(consumer, permissions);
dismiss();
}
private void onAuthorizationError(Exception e)
{
progressGroup.setVisibility(View.INVISIBLE);
errorGroup.setVisibility(View.VISIBLE);
if(webView != null) webView.setVisibility(View.INVISIBLE);
Log.e(TAG, "Error during authorization", e);
}
/* ------------------------------------------------------------------------------------------ */
/** Retrieves the request token asynchronously and returns the url to the website the user has
* to authorize this application. */
private class RetrieveRequestTokenTask extends InlineAsyncTask<String>
{
@Override protected String doInBackground() throws OAuthException
{
return provider.retrieveRequestToken(consumer, CALLBACK_URL);
}
@Override public void onSuccess(String result)
{
authorizeURL = result;
continueAuthentication();
}
@Override public void onError(Exception e)
{
onAuthorizationError(e);
}
}
private class RetrieveVerificationCodeListener implements AsyncTaskListener<String>
{
@Override public void onSuccess(String verifier)
{
verificationCode = verifier;
continueAuthentication();
}
@Override public void onError(Exception e)
{
onAuthorizationError(e);
}
}
/** Retrieves the access token asynchronously */
private class RetrieveAccessTokenTask extends InlineAsyncTask<List<String>>
{
@Override protected List<String> doInBackground() throws OAuthException
{
provider.retrieveAccessToken(consumer, verificationCode);
// must use an own connection here and not the normal singleton because since the
// authorization process is not finished, the new authorized consumer is not applied yet
OsmConnection osm = OsmModule.osmConnection(consumer);
return new PermissionsDao(osm).get();
}
@Override public void onSuccess(List<String> result)
{
if(getActivity() != null) finishAuthentication(result);
}
@Override public void onError(Exception e)
{
if(getActivity() != null) onAuthorizationError(e);
}
}
}