From b6e72a94be90ae7bffddf4ce06ea135130118871 Mon Sep 17 00:00:00 2001 From: Vavassor Date: Tue, 25 Apr 2017 07:30:57 -0400 Subject: [PATCH] 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. --- .../keylesspalace/tusky/AccountActivity.java | 19 ++++-- .../com/keylesspalace/tusky/BaseFragment.java | 7 ++ .../keylesspalace/tusky/CustomTabURLSpan.java | 2 +- .../tusky/DownsizeImageTask.java | 2 + .../com/keylesspalace/tusky/LinkHelper.java | 67 +++++++++++++++++++ .../com/keylesspalace/tusky/LinkListener.java | 6 ++ .../keylesspalace/tusky/LoginActivity.java | 46 +++++++++++-- .../com/keylesspalace/tusky/SFragment.java | 4 +- .../tusky/StatusActionListener.java | 4 +- .../keylesspalace/tusky/StatusViewHolder.java | 51 +------------- .../keylesspalace/tusky/TimelineFragment.java | 5 +- .../keylesspalace/tusky/ViewTagActivity.java | 12 +++- .../tusky/ViewThreadActivity.java | 14 +++- .../tusky/ViewThreadFragment.java | 7 +- .../keylesspalace/tusky/entity/Status.java | 3 + 15 files changed, 180 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/LinkHelper.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/LinkListener.java diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index df7afd398..63d16be66 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -33,7 +33,6 @@ import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; -import android.text.method.LinkMovementMethod; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -206,9 +205,21 @@ public class AccountActivity extends BaseActivity implements SFragment.OnUserRem displayName.setText(account.getDisplayName()); - note.setText(account.note); - note.setLinksClickable(true); - note.setMovementMethod(LinkMovementMethod.getInstance()); + LinkHelper.setClickableText(note, account.note, null, new LinkListener() { + @Override + 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) { accountLockedView.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseFragment.java b/app/src/main/java/com/keylesspalace/tusky/BaseFragment.java index 79569e82f..0a9dcbcf3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseFragment.java @@ -15,6 +15,8 @@ package com.keylesspalace.tusky; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -40,4 +42,9 @@ public class BaseFragment extends Fragment { } super.onDestroy(); } + + protected SharedPreferences getPrivatePreferences() { + return getContext().getSharedPreferences( + getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/CustomTabURLSpan.java b/app/src/main/java/com/keylesspalace/tusky/CustomTabURLSpan.java index dce7d45c4..7af9a84fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/CustomTabURLSpan.java +++ b/app/src/main/java/com/keylesspalace/tusky/CustomTabURLSpan.java @@ -54,7 +54,7 @@ class CustomTabURLSpan extends URLSpan { customTabsIntent.launchUrl(context, uri); } } 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()); } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java b/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java index e9fcc11a9..4e0d77db9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java +++ b/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java @@ -21,6 +21,7 @@ import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.net.Uri; import android.os.AsyncTask; +import android.support.annotation.Nullable; import android.support.media.ExifInterface; import java.io.ByteArrayOutputStream; @@ -42,6 +43,7 @@ class DownsizeImageTask extends AsyncTask { this.listener = listener; } + @Nullable private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) { Matrix matrix = new Matrix(); switch (orientation) { diff --git a/app/src/main/java/com/keylesspalace/tusky/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/LinkHelper.java new file mode 100644 index 000000000..e2b2fedcf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/LinkHelper.java @@ -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()); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/LinkListener.java b/app/src/main/java/com/keylesspalace/tusky/LinkListener.java new file mode 100644 index 000000000..de0242bc1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/LinkListener.java @@ -0,0 +1,6 @@ +package com.keylesspalace.tusky; + +interface LinkListener { + void onViewTag(String tag); + void onViewAccount(String id); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java index 100bb01e6..3854a48e7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java @@ -16,6 +16,7 @@ package com.keylesspalace.tusky; import android.app.AlertDialog; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -24,6 +25,8 @@ import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.customtabs.CustomTabsIntent; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.text.method.LinkMovementMethod; import android.view.View; @@ -224,6 +227,36 @@ public class LoginActivity extends AppCompatActivity { 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) { /* 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. */ @@ -235,11 +268,14 @@ public class LoginActivity extends AppCompatActivity { parameters.put("response_type", "code"); parameters.put("scope", OAUTH_SCOPES); String url = "https://" + domain + endpoint + "?" + toQueryString(parameters); - Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - if (viewIntent.resolveActivity(getPackageManager()) != null) { - startActivity(viewIntent); - } else { - editText.setError(getString(R.string.error_no_web_browser_found)); + Uri uri = Uri.parse(url); + if (!openInCustomTab(uri, this)) { + Intent viewIntent = new Intent(Intent.ACTION_VIEW, uri); + if (viewIntent.resolveActivity(getPackageManager()) != null) { + startActivity(viewIntent); + } else { + editText.setError(getString(R.string.error_no_web_browser_found)); + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/SFragment.java index 3fb124b2c..3af5cce0c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/SFragment.java @@ -15,7 +15,6 @@ package com.keylesspalace.tusky; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -60,8 +59,7 @@ public abstract class SFragment extends BaseFragment { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - SharedPreferences preferences = getContext().getSharedPreferences( - getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + SharedPreferences preferences = getPrivatePreferences(); loggedInAccountId = preferences.getString("loggedInAccountId", null); loggedInUsername = preferences.getString("loggedInAccountUsername", null); } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java index 9d14ed96b..1004bb824 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java @@ -19,13 +19,11 @@ import android.view.View; import com.keylesspalace.tusky.entity.Status; -interface StatusActionListener { +interface StatusActionListener extends LinkListener { void onReply(int position); void onReblog(final boolean reblog, final int position); void onFavourite(final boolean favourite, final int position); void onMore(View view, final int position); void onViewMedia(String url, Status.MediaAttachment.Type type); void onViewThread(int position); - void onViewTag(String tag); - void onViewAccount(String id); } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java index 682155287..5f260b6d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java @@ -102,57 +102,10 @@ class StatusViewHolder extends RecyclerView.ViewHolder { } 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 * account pages. */ - SpannableStringBuilder builder = new SpannableStringBuilder(content); - 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()); + LinkHelper.setClickableText(this.content, content, mentions, listener); } private void setAvatar(String url) { diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java index 6973524a9..d23bbed3a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java @@ -39,7 +39,10 @@ import retrofit2.Call; import retrofit2.Callback; 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 Call> listCall; diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java index 0536330c5..9bce54a14 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -26,7 +26,9 @@ import android.view.MenuItem; import butterknife.BindView; 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; @Override @@ -51,6 +53,8 @@ public class ViewTagActivity extends BaseActivity { Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.TAG, hashtag); fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.commit(); + + timelineFragment = fragment; } @Override @@ -63,4 +67,10 @@ public class ViewTagActivity extends BaseActivity { } return super.onOptionsItemSelected(item); } + + @Override + public void onUserRemoved(String accountId) { + StatusRemoveListener listener = (StatusRemoveListener) timelineFragment; + listener.removePostsByUser(accountId); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java index cee1d2445..9dcd5efac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -24,7 +24,9 @@ import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; -public class ViewThreadActivity extends BaseActivity { +public class ViewThreadActivity extends BaseActivity implements SFragment.OnUserRemovedListener { + Fragment viewThreadFragment; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -44,6 +46,8 @@ public class ViewThreadActivity extends BaseActivity { Fragment fragment = ViewThreadFragment.newInstance(id); fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.commit(); + + viewThreadFragment = fragment; } @Override @@ -62,4 +66,12 @@ public class ViewThreadActivity extends BaseActivity { } return super.onOptionsItemSelected(item); } + + @Override + public void onUserRemoved(String accountId) { + if (viewThreadFragment instanceof StatusRemoveListener) { + StatusRemoveListener listener = (StatusRemoveListener) viewThreadFragment; + listener.removePostsByUser(accountId); + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java index 39baf8ee6..0cf476005 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java @@ -36,7 +36,7 @@ import retrofit2.Call; import retrofit2.Callback; public class ViewThreadFragment extends SFragment implements - SwipeRefreshLayout.OnRefreshListener, StatusActionListener { + SwipeRefreshLayout.OnRefreshListener, StatusActionListener, StatusRemoveListener { private static final String TAG = "ViewThreadFragment"; 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() { sendStatusRequest(thisThreadsStatusId); sendThreadRequest(thisThreadsStatusId); diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java index c793e239f..4e147358a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java @@ -146,5 +146,8 @@ public class Status { @SerializedName("acct") public String username; + + @SerializedName("username") + public String localUsername; } }