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
|
2017-03-21 02:44:30 +01:00
|
|
|
* Lesser General Public License as published by the Free Software Foundation, either version 3 of
|
|
|
|
* the License, or (at your option) any later version.
|
2017-01-20 09:09:10 +01:00
|
|
|
*
|
|
|
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
2017-03-21 02:44:30 +01:00
|
|
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
|
|
|
|
* General Public License for more details.
|
2017-01-20 09:09:10 +01:00
|
|
|
*
|
2017-03-21 02:44:30 +01:00
|
|
|
* You should have received a copy of the GNU Lesser General Public License along with Tusky. If
|
|
|
|
* not, see <http://www.gnu.org/licenses/>. */
|
2017-01-20 09:09:10 +01:00
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
package com.keylesspalace.tusky;
|
|
|
|
|
2017-02-05 05:20:19 +01:00
|
|
|
import android.app.AlertDialog;
|
2017-01-03 00:30:27 +01:00
|
|
|
import android.content.Context;
|
2017-02-05 05:20:19 +01:00
|
|
|
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;
|
2017-03-29 06:22:14 +02:00
|
|
|
import android.preference.PreferenceManager;
|
2017-03-15 20:27:49 +01:00
|
|
|
import android.support.v7.app.AppCompatActivity;
|
2017-03-16 01:01:23 +01:00
|
|
|
import android.text.method.LinkMovementMethod;
|
2017-01-03 00:30:27 +01:00
|
|
|
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
|
|
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.Map;
|
|
|
|
|
2017-03-14 12:59:52 +01:00
|
|
|
import butterknife.BindView;
|
|
|
|
import butterknife.ButterKnife;
|
2017-03-10 03:55:07 +01:00
|
|
|
import retrofit2.Call;
|
|
|
|
import retrofit2.Callback;
|
|
|
|
import retrofit2.Response;
|
2017-03-13 19:13:49 +01:00
|
|
|
import retrofit2.Retrofit;
|
|
|
|
import retrofit2.converter.gson.GsonConverterFactory;
|
2017-03-10 03:55:07 +01:00
|
|
|
|
2017-03-15 20:27:49 +01:00
|
|
|
public class LoginActivity extends AppCompatActivity {
|
2017-03-14 01:49:12 +01:00
|
|
|
private static final String TAG = "LoginActivity"; // logging tag
|
2017-01-07 23:24:02 +01:00
|
|
|
private static String OAUTH_SCOPES = "read write follow";
|
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
private SharedPreferences preferences;
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
private String domain;
|
|
|
|
private String clientId;
|
|
|
|
private String clientSecret;
|
2017-03-14 12:59:52 +01:00
|
|
|
|
|
|
|
@BindView(R.id.edit_text_domain) EditText editText;
|
|
|
|
@BindView(R.id.button_login) Button button;
|
|
|
|
@BindView(R.id.no_account) TextView noAccount;
|
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-03-20 04:15:36 +01:00
|
|
|
private static 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. */
|
2017-03-20 04:15:36 +01:00
|
|
|
private static String validateDomain(String s) {
|
2017-01-03 00:30:27 +01:00
|
|
|
s = s.replaceFirst("http://", "");
|
|
|
|
s = s.replaceFirst("https://", "");
|
2017-03-15 20:32:26 +01:00
|
|
|
return s.trim();
|
2017-01-03 00:30:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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. */
|
2017-03-10 21:12:40 +01:00
|
|
|
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");
|
2017-01-07 23:24:02 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-03-13 19:13:49 +01:00
|
|
|
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. */
|
2017-02-20 01:27:15 +01:00
|
|
|
String prefClientId = preferences.getString(domain + "/client_id", null);
|
|
|
|
String prefClientSecret = preferences.getString(domain + "/client_secret", null);
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-02-20 01:27:15 +01:00
|
|
|
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) {
|
2017-03-14 01:49:12 +01:00
|
|
|
if (!response.isSuccessful()) {
|
2017-03-14 12:59:52 +01:00
|
|
|
editText.setError(getString(R.string.error_failed_app_registration));
|
2017-03-14 01:49:12 +01:00
|
|
|
Log.e(TAG, "App authentication failed. " + response.message());
|
|
|
|
return;
|
|
|
|
}
|
2017-03-10 03:55:07 +01:00
|
|
|
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) {
|
2017-03-14 12:59:52 +01:00
|
|
|
editText.setError(getString(R.string.error_failed_app_registration));
|
2017-03-10 03:55:07 +01:00
|
|
|
t.printStackTrace();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-03-14 12:59:52 +01:00
|
|
|
try {
|
|
|
|
getApiFor(domain).authenticateApp(getString(R.string.app_name), getOauthRedirectUri(), OAUTH_SCOPES,
|
|
|
|
getString(R.string.app_website)).enqueue(callback);
|
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
|
editText.setError(getString(R.string.error_invalid_domain));
|
|
|
|
}
|
2017-03-10 03:55:07 +01:00
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
|
|
super.onCreate(savedInstanceState);
|
2017-03-29 06:22:14 +02:00
|
|
|
|
|
|
|
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
|
|
|
|
setTheme(R.style.AppTheme_Light);
|
|
|
|
}
|
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
setContentView(R.layout.activity_login);
|
2017-03-14 12:59:52 +01:00
|
|
|
ButterKnife.bind(this);
|
|
|
|
|
2017-02-20 01:27:15 +01:00
|
|
|
if (savedInstanceState != null) {
|
|
|
|
domain = savedInstanceState.getString("domain");
|
|
|
|
clientId = savedInstanceState.getString("clientId");
|
|
|
|
clientSecret = savedInstanceState.getString("clientSecret");
|
|
|
|
}
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
preferences = getSharedPreferences(
|
|
|
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
button.setOnClickListener(new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
|
|
|
onButtonClick(editText);
|
|
|
|
}
|
|
|
|
});
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-02-05 05:20:19 +01:00
|
|
|
final Context context = this;
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-02-05 05:20:19 +01:00
|
|
|
noAccount.setOnClickListener(new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
2017-03-16 01:01:23 +01:00
|
|
|
AlertDialog dialog = new AlertDialog.Builder(context)
|
2017-02-05 05:20:19 +01:00
|
|
|
.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-03-16 01:01:23 +01:00
|
|
|
TextView textView = (TextView) dialog.findViewById(android.R.id.message);
|
|
|
|
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
2017-02-05 05:20:19 +01:00
|
|
|
}
|
|
|
|
});
|
2017-01-03 00:30:27 +01:00
|
|
|
}
|
|
|
|
|
2017-02-20 01:27:15 +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
|
2017-03-28 04:13:55 +02:00
|
|
|
protected void onStop() {
|
|
|
|
super.onStop();
|
2017-01-03 00:30:27 +01:00
|
|
|
SharedPreferences.Editor editor = preferences.edit();
|
|
|
|
editor.putString("domain", domain);
|
|
|
|
editor.putString("clientId", clientId);
|
|
|
|
editor.putString("clientSecret", clientSecret);
|
2017-02-17 03:11:05 +01:00
|
|
|
editor.apply();
|
2017-01-03 00:30:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private void onLoginSuccess(String accessToken) {
|
2017-02-17 03:11:05 +01:00
|
|
|
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
|
2017-03-28 04:13:55 +02:00
|
|
|
protected void onStart() {
|
|
|
|
super.onStart();
|
2017-01-03 00:30:27 +01:00
|
|
|
/* 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();
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-03-15 20:32:26 +01:00
|
|
|
preferences = getSharedPreferences(
|
|
|
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
|
|
|
|
2017-03-20 04:15:36 +01:00
|
|
|
if (preferences.getString("accessToken", null) != null
|
|
|
|
&& preferences.getString("domain", null) != null) {
|
2017-03-15 20:32:26 +01:00
|
|
|
// We are already logged in, go to MainActivity
|
|
|
|
Intent intent = new Intent(this, MainActivity.class);
|
|
|
|
startActivity(intent);
|
|
|
|
finish();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
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");
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-01-03 00:30:27 +01:00
|
|
|
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. */
|
|
|
|
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) {
|
2017-03-14 01:49:12 +01:00
|
|
|
if (response.isSuccessful()) {
|
|
|
|
onLoginSuccess(response.body().accessToken);
|
|
|
|
} else {
|
2017-03-20 04:15:36 +01:00
|
|
|
editText.setError(getString(R.string.error_retrieving_oauth_token));
|
|
|
|
Log.e(TAG, String.format("%s %s",
|
|
|
|
getString(R.string.error_retrieving_oauth_token),
|
|
|
|
response.message()));
|
2017-03-14 01:49:12 +01:00
|
|
|
}
|
2017-03-10 03:55:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onFailure(Call<AccessToken> call, Throwable t) {
|
2017-03-20 04:15:36 +01:00
|
|
|
editText.setError(getString(R.string.error_retrieving_oauth_token));
|
|
|
|
Log.e(TAG, String.format("%s %s",
|
|
|
|
getString(R.string.error_retrieving_oauth_token),
|
|
|
|
t.getMessage()));
|
2017-03-10 03:55:07 +01:00
|
|
|
}
|
|
|
|
};
|
2017-03-14 12:59:52 +01:00
|
|
|
|
2017-03-13 19:13:49 +01:00
|
|
|
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. */
|
2017-03-20 04:15:36 +01:00
|
|
|
editText.setError(getString(R.string.error_authorization_denied));
|
|
|
|
Log.e(TAG, getString(R.string.error_authorization_denied) + error);
|
2017-01-03 00:30:27 +01:00
|
|
|
} else {
|
|
|
|
// This case means a junk response was received somehow.
|
2017-03-14 12:59:52 +01:00
|
|
|
editText.setError(getString(R.string.error_authorization_unknown));
|
2017-01-03 00:30:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|