Tusky-App-Android/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java

270 lines
11 KiB
Java
Raw Normal View History

2017-01-20 09:09:10 +01:00
/* Copyright 2017 Andrew Dawson
*
* This file is part of Tusky.
*
* Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky. If not, see
* <http://www.gnu.org/licenses/>. */
2017-01-03 00:30:27 +01:00
package com.keylesspalace.tusky;
import android.app.AlertDialog;
2017-01-03 00:30:27 +01:00
import android.content.Context;
import android.content.DialogInterface;
2017-01-03 00:30:27 +01:00
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
2017-03-10 03:55:07 +01:00
import com.keylesspalace.tusky.entity.AccessToken;
import com.keylesspalace.tusky.entity.AppCredentials;
2017-01-03 00:30:27 +01:00
2017-03-10 03:55:07 +01:00
import java.util.ArrayList;
2017-01-03 00:30:27 +01:00
import java.util.HashMap;
2017-03-10 03:55:07 +01:00
import java.util.List;
2017-01-03 00:30:27 +01:00
import java.util.Map;
2017-03-10 03:55:07 +01:00
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
2017-03-10 03:55:07 +01:00
public class LoginActivity extends BaseActivity {
private static String OAUTH_SCOPES = "read write follow";
2017-01-03 00:30:27 +01:00
private SharedPreferences preferences;
private String domain;
private String clientId;
private String clientSecret;
2017-03-07 15:03:41 +01:00
private EditText editText;
2017-01-03 00:30:27 +01:00
/**
* Chain together the key-value pairs into a query string, for either appending to a URL or
* as the content of an HTTP request.
*/
2017-02-27 01:14:50 +01:00
private String toQueryString(Map<String, String> parameters) {
2017-01-03 00:30:27 +01:00
StringBuilder s = new StringBuilder();
String between = "";
for (Map.Entry<String, String> entry : parameters.entrySet()) {
s.append(between);
2017-02-27 01:14:50 +01:00
s.append(entry.getKey());
2017-01-03 00:30:27 +01:00
s.append("=");
2017-02-27 01:14:50 +01:00
s.append(entry.getValue());
2017-01-03 00:30:27 +01:00
between = "&";
}
return s.toString();
}
/** Make sure the user-entered text is just a fully-qualified domain name. */
private String validateDomain(String s) {
s = s.replaceFirst("http://", "");
s = s.replaceFirst("https://", "");
return s;
}
private String getOauthRedirectUri() {
String scheme = getString(R.string.oauth_scheme);
String host = getString(R.string.oauth_redirect_host);
return scheme + "://" + host + "/";
}
private void redirectUserToAuthorizeAndLogin() {
/* To authorize this app and log in it's necessary to redirect to the domain given,
* activity_login there, and the server will redirect back to the app with its response. */
String endpoint = MastodonAPI.ENDPOINT_AUTHORIZE;
2017-01-03 00:30:27 +01:00
String redirectUri = getOauthRedirectUri();
Map<String, String> parameters = new HashMap<>();
parameters.put("client_id", clientId);
parameters.put("redirect_uri", redirectUri);
parameters.put("response_type", "code");
parameters.put("scope", OAUTH_SCOPES);
2017-02-27 01:14:50 +01:00
String url = "https://" + domain + endpoint + "?" + toQueryString(parameters);
2017-01-03 00:30:27 +01:00
Intent viewIntent = new Intent("android.intent.action.VIEW", Uri.parse(url));
startActivity(viewIntent);
}
private MastodonAPI getApiFor(String domain) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain)
.addConverterFactory(GsonConverterFactory.create())
.build();
return retrofit.create(MastodonAPI.class);
}
2017-01-03 00:30:27 +01:00
/**
* Obtain the oauth client credentials for this app. This is only necessary the first time the
* app is run on a given server instance. So, after the first authentication, they are
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
*/
private void onButtonClick(final EditText editText) {
domain = validateDomain(editText.getText().toString());
/* Attempt to get client credentials from SharedPreferences, and if not present
* (such as in the case that the domain has never been accessed before)
* authenticate with the server and store the received credentials to use next
* time. */
String prefClientId = preferences.getString(domain + "/client_id", null);
String prefClientSecret = preferences.getString(domain + "/client_secret", null);
if (prefClientId != null && prefClientSecret != null) {
clientId = prefClientId;
clientSecret = prefClientSecret;
2017-01-03 00:30:27 +01:00
redirectUserToAuthorizeAndLogin();
} else {
2017-03-10 03:55:07 +01:00
Callback<AppCredentials> callback = new Callback<AppCredentials>() {
@Override
public void onResponse(Call<AppCredentials> call, Response<AppCredentials> response) {
AppCredentials credentials = response.body();
clientId = credentials.clientId;
clientSecret = credentials.clientSecret;
SharedPreferences.Editor editor = preferences.edit();
editor.putString(domain + "/client_id", clientId);
editor.putString(domain + "/client_secret", clientSecret);
editor.apply();
redirectUserToAuthorizeAndLogin();
}
@Override
public void onFailure(Call<AppCredentials> call, Throwable t) {
editText.setError(
"This app could not obtain authentication from that server " +
"instance.");
t.printStackTrace();
}
};
getApiFor(domain).authenticateApp(getString(R.string.app_name), getOauthRedirectUri(), OAUTH_SCOPES,
2017-03-10 03:55:07 +01:00
getString(R.string.app_website)).enqueue(callback);
2017-01-03 00:30:27 +01:00
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
if (savedInstanceState != null) {
domain = savedInstanceState.getString("domain");
clientId = savedInstanceState.getString("clientId");
clientSecret = savedInstanceState.getString("clientSecret");
}
2017-01-03 00:30:27 +01:00
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
Button button = (Button) findViewById(R.id.button_login);
2017-03-07 15:03:41 +01:00
editText = (EditText) findViewById(R.id.edit_text_domain);
2017-01-03 00:30:27 +01:00
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onButtonClick(editText);
}
});
TextView noAccount = (TextView) findViewById(R.id.no_account);
final Context context = this;
noAccount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialog.Builder(context)
.setMessage(R.string.dialog_no_account)
.setPositiveButton(R.string.action_close,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
}
});
2017-01-03 00:30:27 +01:00
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("domain", domain);
outState.putString("clientId", clientId);
outState.putString("clientSecret", clientSecret);
super.onSaveInstanceState(outState);
}
2017-01-03 00:30:27 +01:00
@Override
protected void onPause() {
super.onPause();
SharedPreferences.Editor editor = preferences.edit();
editor.putString("domain", domain);
editor.putString("clientId", clientId);
editor.putString("clientSecret", clientSecret);
editor.apply();
2017-01-03 00:30:27 +01:00
}
private void onLoginSuccess(String accessToken) {
preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
2017-01-03 00:30:27 +01:00
SharedPreferences.Editor editor = preferences.edit();
editor.putString("accessToken", accessToken);
editor.apply();
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
@Override
protected void onResume() {
super.onResume();
/* Check if we are resuming during authorization by seeing if the intent contains the
* redirect that was given to the server. If so, its response is here! */
Uri uri = getIntent().getData();
String redirectUri = getOauthRedirectUri();
if (uri != null && uri.toString().startsWith(redirectUri)) {
// This should either have returned an authorization code or an error.
String code = uri.getQueryParameter("code");
String error = uri.getQueryParameter("error");
final TextView errorText = (TextView) findViewById(R.id.text_error);
if (code != null) {
/* During the redirect roundtrip this Activity usually dies, which wipes out the
* instance variables, so they have to be recovered from where they were saved in
* SharedPreferences. */
preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
2017-01-03 00:30:27 +01:00
domain = preferences.getString("domain", null);
clientId = preferences.getString("clientId", null);
clientSecret = preferences.getString("clientSecret", null);
/* Since authorization has succeeded, the final step to log in is to exchange
* the authorization code for an access token. */
2017-03-10 03:55:07 +01:00
Callback<AccessToken> callback = new Callback<AccessToken>() {
@Override
public void onResponse(Call<AccessToken> call, Response<AccessToken> response) {
onLoginSuccess(response.body().accessToken);
}
@Override
public void onFailure(Call<AccessToken> call, Throwable t) {
editText.setError(t.getMessage());
}
};
getApiFor(domain).fetchOAuthToken(clientId, clientSecret, redirectUri, code,
2017-03-10 03:55:07 +01:00
"authorization_code").enqueue(callback);
2017-01-03 00:30:27 +01:00
} else if (error != null) {
/* Authorization failed. Put the error response where the user can read it and they
* can try again. */
errorText.setText(error);
} else {
// This case means a junk response was received somehow.
errorText.setText(getString(R.string.error_authorization_unknown));
2017-01-03 00:30:27 +01:00
}
}
}
}