Custom tabs are now used for login and links on account pages, with a fallback to the default browser if not supported.

Also, fixes crashes when entering tag and threads due to me forgetting to implement the interfaces required by the code that removes posts from timelines when blocking/muting.

Also fixes a small bug where for mentions of users from other instances, clicking on the mention would open the profile in the browser instead of in-app.
This commit is contained in:
Vavassor 2017-04-25 07:30:57 -04:00
parent f9722ac2c2
commit b6e72a94be
15 changed files with 180 additions and 69 deletions

View file

@ -33,7 +33,6 @@ import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.text.method.LinkMovementMethod;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -206,9 +205,21 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem
displayName.setText(account.getDisplayName()); displayName.setText(account.getDisplayName());
note.setText(account.note); LinkHelper.setClickableText(note, account.note, null, new LinkListener() {
note.setLinksClickable(true); @Override
note.setMovementMethod(LinkMovementMethod.getInstance()); public void onViewTag(String tag) {
Intent intent = new Intent(AccountActivity.this, ViewTagActivity.class);
intent.putExtra("hashtag", tag);
startActivity(intent);
}
@Override
public void onViewAccount(String id) {
Intent intent = new Intent(AccountActivity.this, AccountActivity.class);
intent.putExtra("id", id);
startActivity(intent);
}
});
if (account.locked) { if (account.locked) {
accountLockedView.setVisibility(View.VISIBLE); accountLockedView.setVisibility(View.VISIBLE);

View file

@ -15,6 +15,8 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
@ -40,4 +42,9 @@ public class BaseFragment extends Fragment {
} }
super.onDestroy(); super.onDestroy();
} }
protected SharedPreferences getPrivatePreferences() {
return getContext().getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
}
} }

View file

@ -54,7 +54,7 @@ class CustomTabURLSpan extends URLSpan {
customTabsIntent.launchUrl(context, uri); customTabsIntent.launchUrl(context, uri);
} }
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
android.util.Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString()); Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
} }
} }
} }

View file

@ -21,6 +21,7 @@ import android.graphics.BitmapFactory;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.support.media.ExifInterface; import android.support.media.ExifInterface;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -42,6 +43,7 @@ class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
this.listener = listener; this.listener = listener;
} }
@Nullable
private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) { private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
Matrix matrix = new Matrix(); Matrix matrix = new Matrix();
switch (orientation) { switch (orientation) {

View file

@ -0,0 +1,67 @@
package com.keylesspalace.tusky;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.View;
import android.widget.TextView;
import com.keylesspalace.tusky.entity.Status;
class LinkHelper {
static void setClickableText(TextView view, Spanned content,
@Nullable Status.Mention[] mentions,
final LinkListener listener) {
SpannableStringBuilder builder = new SpannableStringBuilder(content);
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(view.getContext())
.getBoolean("customTabs", true);
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
for (URLSpan span : urlSpans) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
int flags = builder.getSpanFlags(span);
CharSequence text = builder.subSequence(start, end);
if (text.charAt(0) == '#') {
final String tag = text.subSequence(1, text.length()).toString();
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewTag(tag);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
} else if (text.charAt(0) == '@' && mentions != null) {
final String accountUsername = text.subSequence(1, text.length()).toString();
String id = null;
for (Status.Mention mention : mentions) {
if (mention.localUsername.equals(accountUsername)) {
id = mention.id;
}
}
if (id != null) {
final String accountId = id;
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewAccount(accountId);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
} else if (useCustomTabs) {
ClickableSpan newSpan = new CustomTabURLSpan(span.getURL());
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
}
view.setText(builder);
view.setLinksClickable(true);
view.setMovementMethod(LinkMovementMethod.getInstance());
}
}

View file

@ -0,0 +1,6 @@
package com.keylesspalace.tusky;
interface LinkListener {
void onViewTag(String tag);
void onViewAccount(String id);
}

View file

@ -16,6 +16,7 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
@ -24,6 +25,8 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.View; import android.view.View;
@ -224,6 +227,36 @@ public class LoginActivity extends AppCompatActivity {
return s.toString(); return s.toString();
} }
private static boolean openInCustomTab(Uri uri, Context context) {
boolean lightTheme = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean("lightTheme", false);
int toolbarColorRes;
if (lightTheme) {
toolbarColorRes = R.color.custom_tab_toolbar_light;
} else {
toolbarColorRes = R.color.custom_tab_toolbar_dark;
}
int toolbarColor = ContextCompat.getColor(context, toolbarColorRes);
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(toolbarColor);
CustomTabsIntent customTabsIntent = builder.build();
try {
String packageName = CustomTabsHelper.getPackageNameToUse(context);
/* If we cant find a package name, it means theres no browser that supports
* Chrome Custom Tabs installed. So, we fallback to the webview */
if (packageName == null) {
return false;
} else {
customTabsIntent.intent.setPackage(packageName);
customTabsIntent.launchUrl(context, uri);
}
} catch (ActivityNotFoundException e) {
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
return false;
}
return true;
}
private void redirectUserToAuthorizeAndLogin(EditText editText) { 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. */
@ -235,13 +268,16 @@ 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(Intent.ACTION_VIEW, Uri.parse(url)); Uri uri = Uri.parse(url);
if (!openInCustomTab(uri, this)) {
Intent viewIntent = new Intent(Intent.ACTION_VIEW, uri);
if (viewIntent.resolveActivity(getPackageManager()) != null) { if (viewIntent.resolveActivity(getPackageManager()) != null) {
startActivity(viewIntent); startActivity(viewIntent);
} else { } else {
editText.setError(getString(R.string.error_no_web_browser_found)); editText.setError(getString(R.string.error_no_web_browser_found));
} }
} }
}
@Override @Override
protected void onStop() { protected void onStop() {

View file

@ -15,7 +15,6 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
@ -60,8 +59,7 @@ public abstract class SFragment extends BaseFragment {
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
SharedPreferences preferences = getContext().getSharedPreferences( SharedPreferences preferences = getPrivatePreferences();
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
loggedInAccountId = preferences.getString("loggedInAccountId", null); loggedInAccountId = preferences.getString("loggedInAccountId", null);
loggedInUsername = preferences.getString("loggedInAccountUsername", null); loggedInUsername = preferences.getString("loggedInAccountUsername", null);
} }

View file

@ -19,13 +19,11 @@ import android.view.View;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
interface StatusActionListener { interface StatusActionListener extends LinkListener {
void onReply(int position); void onReply(int position);
void onReblog(final boolean reblog, final int position); void onReblog(final boolean reblog, final int position);
void onFavourite(final boolean favourite, final int position); void onFavourite(final boolean favourite, final int position);
void onMore(View view, final int position); void onMore(View view, final int position);
void onViewMedia(String url, Status.MediaAttachment.Type type); void onViewMedia(String url, Status.MediaAttachment.Type type);
void onViewThread(int position); void onViewThread(int position);
void onViewTag(String tag);
void onViewAccount(String id);
} }

View file

@ -102,57 +102,10 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
} }
private void setContent(Spanned content, Status.Mention[] mentions, private void setContent(Spanned content, Status.Mention[] mentions,
final StatusActionListener listener) { StatusActionListener listener) {
/* Redirect URLSpan's in the status content to the listener for viewing tag pages and /* Redirect URLSpan's in the status content to the listener for viewing tag pages and
* account pages. */ * account pages. */
SpannableStringBuilder builder = new SpannableStringBuilder(content); LinkHelper.setClickableText(this.content, content, mentions, listener);
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(container.getContext()).getBoolean("customTabs", true);
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
for (URLSpan span : urlSpans) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
int flags = builder.getSpanFlags(span);
CharSequence text = builder.subSequence(start, end);
if (text.charAt(0) == '#') {
final String tag = text.subSequence(1, text.length()).toString();
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewTag(tag);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
} else if (text.charAt(0) == '@') {
final String accountUsername = text.subSequence(1, text.length()).toString();
String id = null;
for (Status.Mention mention: mentions) {
if (mention.username.equals(accountUsername)) {
id = mention.id;
}
}
if (id != null) {
final String accountId = id;
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewAccount(accountId);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
} else if (useCustomTabs) {
ClickableSpan newSpan = new CustomTabURLSpan(span.getURL());
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
}
// Set the contents.
this.content.setText(builder);
// Make links clickable.
this.content.setLinksClickable(true);
this.content.setMovementMethod(LinkMovementMethod.getInstance());
} }
private void setAvatar(String url) { private void setAvatar(String url) {

View file

@ -39,7 +39,10 @@ import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
public class TimelineFragment extends SFragment implements public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, StatusRemoveListener, SharedPreferences.OnSharedPreferenceChangeListener { SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
StatusRemoveListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "Timeline"; // logging tag private static final String TAG = "Timeline"; // logging tag
private Call<List<Status>> listCall; private Call<List<Status>> listCall;

View file

@ -26,7 +26,9 @@ import android.view.MenuItem;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
public class ViewTagActivity extends BaseActivity { public class ViewTagActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
private Fragment timelineFragment;
@BindView(R.id.toolbar) Toolbar toolbar; @BindView(R.id.toolbar) Toolbar toolbar;
@Override @Override
@ -51,6 +53,8 @@ public class ViewTagActivity extends BaseActivity {
Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.TAG, hashtag); Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.TAG, hashtag);
fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit(); fragmentTransaction.commit();
timelineFragment = fragment;
} }
@Override @Override
@ -63,4 +67,10 @@ public class ViewTagActivity extends BaseActivity {
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public void onUserRemoved(String accountId) {
StatusRemoveListener listener = (StatusRemoveListener) timelineFragment;
listener.removePostsByUser(accountId);
}
} }

View file

@ -24,7 +24,9 @@ import android.support.v7.widget.Toolbar;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
public class ViewThreadActivity extends BaseActivity { public class ViewThreadActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
Fragment viewThreadFragment;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -44,6 +46,8 @@ public class ViewThreadActivity extends BaseActivity {
Fragment fragment = ViewThreadFragment.newInstance(id); Fragment fragment = ViewThreadFragment.newInstance(id);
fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit(); fragmentTransaction.commit();
viewThreadFragment = fragment;
} }
@Override @Override
@ -62,4 +66,12 @@ public class ViewThreadActivity extends BaseActivity {
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public void onUserRemoved(String accountId) {
if (viewThreadFragment instanceof StatusRemoveListener) {
StatusRemoveListener listener = (StatusRemoveListener) viewThreadFragment;
listener.removePostsByUser(accountId);
}
}
} }

View file

@ -36,7 +36,7 @@ import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
public class ViewThreadFragment extends SFragment implements public class ViewThreadFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener { SwipeRefreshLayout.OnRefreshListener, StatusActionListener, StatusRemoveListener {
private static final String TAG = "ViewThreadFragment"; private static final String TAG = "ViewThreadFragment";
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
@ -150,6 +150,11 @@ public class ViewThreadFragment extends SFragment implements
} }
} }
@Override
public void removePostsByUser(String accountId) {
adapter.removeAllByAccountId(accountId);
}
public void onRefresh() { public void onRefresh() {
sendStatusRequest(thisThreadsStatusId); sendStatusRequest(thisThreadsStatusId);
sendThreadRequest(thisThreadsStatusId); sendThreadRequest(thisThreadsStatusId);

View file

@ -146,5 +146,8 @@ public class Status {
@SerializedName("acct") @SerializedName("acct")
public String username; public String username;
@SerializedName("username")
public String localUsername;
} }
} }