Fixes a crash during authentication and another when opening the composer options. Also, sets up the next update to reset the redirect URI for the app during authorization.

This commit is contained in:
Vavassor 2017-04-04 22:29:15 -04:00
parent 3c655a25dc
commit 2cb0b96abd
5 changed files with 78 additions and 58 deletions

View File

@ -93,7 +93,7 @@ import okhttp3.RequestBody;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
public class ComposeActivity extends BaseActivity { public class ComposeActivity extends BaseActivity implements ComposeOptionsFragment.Listener {
private static final String TAG = "ComposeActivity"; // logging tag private static final String TAG = "ComposeActivity"; // logging tag
private static final int STATUS_CHARACTER_LIMIT = 500; private static final int STATUS_CHARACTER_LIMIT = 500;
private static final int STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB private static final int STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB
@ -525,29 +525,18 @@ public class ComposeActivity extends BaseActivity {
private void showComposeOptions() { private void showComposeOptions() {
ComposeOptionsFragment fragment = ComposeOptionsFragment.newInstance( ComposeOptionsFragment fragment = ComposeOptionsFragment.newInstance(
statusVisibility, statusHideText, inReplyToId != null, statusVisibility, statusHideText, inReplyToId != null);
new ComposeOptionsFragment.Listener() {
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {}
@Override
public void onVisibilityChanged(String visibility) {
setStatusVisibility(visibility);
}
@Override
public void onContentWarningChanged(boolean hideText) {
showContentWarning(hideText);
}
});
fragment.show(getSupportFragmentManager(), null); fragment.show(getSupportFragmentManager(), null);
} }
public void onVisibilityChanged(String visibility) {
setStatusVisibility(visibility);
}
public void onContentWarningChanged(boolean hideText) {
showContentWarning(hideText);
}
private void sendStatus() { private void sendStatus() {
if (statusAlreadyInFlight) { if (statusAlreadyInFlight) {
return; return;

View File

@ -1,5 +1,6 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -12,19 +13,22 @@ import android.widget.CompoundButton;
import android.widget.RadioButton; import android.widget.RadioButton;
import android.widget.RadioGroup; import android.widget.RadioGroup;
import java.util.List;
public class ComposeOptionsFragment extends BottomSheetDialogFragment { public class ComposeOptionsFragment extends BottomSheetDialogFragment {
public interface Listener extends Parcelable { interface Listener {
void onVisibilityChanged(String visibility); void onVisibilityChanged(String visibility);
void onContentWarningChanged(boolean hideText); void onContentWarningChanged(boolean hideText);
} }
private RadioGroup radio;
private CheckBox hideText;
private Listener listener; private Listener listener;
public static ComposeOptionsFragment newInstance(String visibility, public static ComposeOptionsFragment newInstance(String visibility, boolean hideText,
boolean hideText, boolean isReply, Listener listener) { boolean isReply) {
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
ComposeOptionsFragment fragment = new ComposeOptionsFragment(); ComposeOptionsFragment fragment = new ComposeOptionsFragment();
arguments.putParcelable("listener", listener);
arguments.putString("visibility", visibility); arguments.putString("visibility", visibility);
arguments.putBoolean("hideText", hideText); arguments.putBoolean("hideText", hideText);
arguments.putBoolean("isReply", isReply); arguments.putBoolean("isReply", isReply);
@ -32,6 +36,12 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
return fragment; return fragment;
} }
@Override
public void onAttach(Context context) {
super.onAttach(context);
listener = (Listener) context;
}
@Nullable @Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@ -39,12 +49,11 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
View rootView = inflater.inflate(R.layout.fragment_compose_options, container, false); View rootView = inflater.inflate(R.layout.fragment_compose_options, container, false);
Bundle arguments = getArguments(); Bundle arguments = getArguments();
listener = arguments.getParcelable("listener");
String statusVisibility = arguments.getString("visibility"); String statusVisibility = arguments.getString("visibility");
boolean statusHideText = arguments.getBoolean("hideText"); boolean statusHideText = arguments.getBoolean("hideText");
boolean isReply = arguments.getBoolean("isReply"); boolean isReply = arguments.getBoolean("isReply");
RadioGroup radio = (RadioGroup) rootView.findViewById(R.id.radio_visibility); radio = (RadioGroup) rootView.findViewById(R.id.radio_visibility);
int radioCheckedId; int radioCheckedId;
if (!isReply) { if (!isReply) {
radioCheckedId = R.id.radio_public; radioCheckedId = R.id.radio_public;
@ -59,6 +68,21 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
} }
} }
radio.check(radioCheckedId); radio.check(radioCheckedId);
if (isReply) {
RadioButton publicButton = (RadioButton) rootView.findViewById(R.id.radio_public);
publicButton.setEnabled(false);
}
hideText = (CheckBox) rootView.findViewById(R.id.compose_hide_text);
hideText.setChecked(statusHideText);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
radio.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { radio.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(RadioGroup group, int checkedId) { public void onCheckedChanged(RadioGroup group, int checkedId) {
@ -81,20 +105,11 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
listener.onVisibilityChanged(visibility); listener.onVisibilityChanged(visibility);
} }
}); });
if (isReply) {
RadioButton publicButton = (RadioButton) rootView.findViewById(R.id.radio_public);
publicButton.setEnabled(false);
}
CheckBox hideText = (CheckBox) rootView.findViewById(R.id.compose_hide_text);
hideText.setChecked(statusHideText);
hideText.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { hideText.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onContentWarningChanged(isChecked); listener.onContentWarningChanged(isChecked);
} }
}); });
return rootView;
} }
} }

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
@ -33,8 +34,6 @@ import android.widget.TextView;
import com.keylesspalace.tusky.entity.AccessToken; import com.keylesspalace.tusky.entity.AccessToken;
import com.keylesspalace.tusky.entity.AppCredentials; import com.keylesspalace.tusky.entity.AppCredentials;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -60,14 +59,6 @@ public class LoginActivity extends AppCompatActivity {
@BindView(R.id.button_login) Button button; @BindView(R.id.button_login) Button button;
@BindView(R.id.whats_an_instance) TextView whatsAnInstance; @BindView(R.id.whats_an_instance) TextView whatsAnInstance;
private static String urlEncode(String string) {
try {
return URLEncoder.encode(string, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Failed to encode the string.", e);
}
}
/** /**
* Chain together the key-value pairs into a query string, for either appending to a URL or * Chain together the key-value pairs into a query string, for either appending to a URL or
* as the content of an HTTP request. * as the content of an HTTP request.
@ -77,9 +68,9 @@ public class LoginActivity extends AppCompatActivity {
String between = ""; String between = "";
for (Map.Entry<String, String> entry : parameters.entrySet()) { for (Map.Entry<String, String> entry : parameters.entrySet()) {
s.append(between); s.append(between);
s.append(urlEncode(entry.getKey())); s.append(Uri.encode(entry.getKey()));
s.append("="); s.append("=");
s.append(urlEncode(entry.getValue())); s.append(Uri.encode(entry.getValue()));
between = "&"; between = "&";
} }
return s.toString(); return s.toString();
@ -98,7 +89,7 @@ public class LoginActivity extends AppCompatActivity {
return scheme + "://" + host + "/"; return scheme + "://" + host + "/";
} }
private void redirectUserToAuthorizeAndLogin() { private void redirectUserToAuthorizeAndLogin(EditText editText) {
/* To authorize this app and log in it's necessary to redirect to the domain given, /* 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. */ * activity_login there, and the server will redirect back to the app with its response. */
String endpoint = MastodonAPI.ENDPOINT_AUTHORIZE; String endpoint = MastodonAPI.ENDPOINT_AUTHORIZE;
@ -109,8 +100,12 @@ public class LoginActivity extends AppCompatActivity {
parameters.put("response_type", "code"); parameters.put("response_type", "code");
parameters.put("scope", OAUTH_SCOPES); parameters.put("scope", OAUTH_SCOPES);
String url = "https://" + domain + endpoint + "?" + toQueryString(parameters); String url = "https://" + domain + endpoint + "?" + toQueryString(parameters);
Intent viewIntent = new Intent("android.intent.action.VIEW", Uri.parse(url)); Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(viewIntent); if (viewIntent.resolveActivity(getPackageManager()) != null) {
startActivity(viewIntent);
} else {
editText.setError(getString(R.string.error_no_web_browser_found));
}
} }
private MastodonAPI getApiFor(String domain) { private MastodonAPI getApiFor(String domain) {
@ -139,7 +134,7 @@ public class LoginActivity extends AppCompatActivity {
if (prefClientId != null && prefClientSecret != null) { if (prefClientId != null && prefClientSecret != null) {
clientId = prefClientId; clientId = prefClientId;
clientSecret = prefClientSecret; clientSecret = prefClientSecret;
redirectUserToAuthorizeAndLogin(); redirectUserToAuthorizeAndLogin(editText);
} else { } else {
Callback<AppCredentials> callback = new Callback<AppCredentials>() { Callback<AppCredentials> callback = new Callback<AppCredentials>() {
@Override @Override
@ -156,7 +151,7 @@ public class LoginActivity extends AppCompatActivity {
editor.putString(domain + "/client_id", clientId); editor.putString(domain + "/client_id", clientId);
editor.putString(domain + "/client_secret", clientSecret); editor.putString(domain + "/client_secret", clientSecret);
editor.apply(); editor.apply();
redirectUserToAuthorizeAndLogin(); redirectUserToAuthorizeAndLogin(editText);
} }
@Override @Override
@ -225,6 +220,26 @@ public class LoginActivity extends AppCompatActivity {
textView.setMovementMethod(LinkMovementMethod.getInstance()); textView.setMovementMethod(LinkMovementMethod.getInstance());
} }
}); });
// Apply any updates needed.
int versionCode = 1;
try {
versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "The app version was not found. " + e.getMessage());
}
if (preferences.getInt("lastUpdate", 0) != versionCode) {
SharedPreferences.Editor editor = preferences.edit();
if (versionCode == 14) {
/* This version switches the order of scheme and host in the OAuth redirect URI.
* But to fix it requires forcing the app to re-authenticate with servers. So, clear
* out the stored client id/secret pairs. The only other things that are lost are
* "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
editor.clear();
}
editor.putInt("lastUpdate", versionCode);
editor.apply();
}
} }
@Override @Override

View File

@ -4,7 +4,7 @@
<string name="app_website">http://tusky.keylesspalace.com</string> <string name="app_website">http://tusky.keylesspalace.com</string>
<string name="tusky_api_url">https://tuskynotifier.keylesspalace.com</string> <string name="tusky_api_url">https://tuskynotifier.keylesspalace.com</string>
<string name="oauth_scheme">com.keylesspalace.tusky</string> <string name="oauth_scheme">oauth2redirect</string>
<string name="oauth_redirect_host">oauth2redirect</string> <string name="oauth_redirect_host">com.keylesspalace.tusky</string>
<string name="preferences_file_key">com.keylesspalace.tusky.PREFERENCES</string> <string name="preferences_file_key">com.keylesspalace.tusky.PREFERENCES</string>
</resources> </resources>

View File

@ -1,8 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="error_no_web_browser_found">Couldn\'t find a web browser to use.</string>
<string name="error_authorization_unknown">An unidentified authorization error occurred.</string> <string name="error_authorization_unknown">An unidentified authorization error occurred.</string>
<string name="error_authorization_denied" tools:ignore="MissingTranslation">Authorization was denied.</string> <string name="error_authorization_denied">Authorization was denied.</string>
<string name="error_retrieving_oauth_token" tools:ignore="MissingTranslation">Failed getting a login token.</string> <string name="error_retrieving_oauth_token">Failed getting a login token.</string>
<string name="error_fetching_notifications">Notifications could not be fetched.</string> <string name="error_fetching_notifications">Notifications could not be fetched.</string>
<string name="error_compose_character_limit">The status is too long!</string> <string name="error_compose_character_limit">The status is too long!</string>
<string name="error_sending_status">The status failed to be sent.</string> <string name="error_sending_status">The status failed to be sent.</string>