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:
parent
f9722ac2c2
commit
b6e72a94be
15 changed files with 180 additions and 69 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Uri, Void, Boolean> {
|
|||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
|
|
67
app/src/main/java/com/keylesspalace/tusky/LinkHelper.java
Normal file
67
app/src/main/java/com/keylesspalace/tusky/LinkHelper.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
interface LinkListener {
|
||||
void onViewTag(String tag);
|
||||
void onViewAccount(String id);
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<List<Status>> listCall;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -146,5 +146,8 @@ public class Status {
|
|||
|
||||
@SerializedName("acct")
|
||||
public String username;
|
||||
|
||||
@SerializedName("username")
|
||||
public String localUsername;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue