fix rtl unicode formatting (#659)

* Isolate usernames when formatting, to improve interaction of RTL usernames with LTR locales (and vice versa)

* Add bidirectionality safeguards in NotificationHelper

* Cache bidirectionality formatter instance in NotificationsAdapter
This commit is contained in:
Levi Bard 2018-05-24 19:00:17 +02:00 committed by Konrad Pozniak
parent e79b47552e
commit 3a8d96346b
2 changed files with 35 additions and 18 deletions

View File

@ -24,6 +24,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v4.text.BidiFormatter;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
@ -65,6 +66,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private NotificationActionListener notificationActionListener; private NotificationActionListener notificationActionListener;
private FooterViewHolder.State footerState; private FooterViewHolder.State footerState;
private boolean mediaPreviewEnabled; private boolean mediaPreviewEnabled;
private BidiFormatter bidiFormatter;
public NotificationsAdapter(StatusActionListener statusListener, public NotificationsAdapter(StatusActionListener statusListener,
NotificationActionListener notificationActionListener) { NotificationActionListener notificationActionListener) {
@ -74,6 +76,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
this.notificationActionListener = notificationActionListener; this.notificationActionListener = notificationActionListener;
footerState = FooterViewHolder.State.END; footerState = FooterViewHolder.State.END;
mediaPreviewEnabled = true; mediaPreviewEnabled = true;
bidiFormatter = BidiFormatter.getInstance();
} }
@NonNull @NonNull
@ -148,7 +151,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
concreteNotificaton.getAccount().getAvatar()); concreteNotificaton.getAccount().getAvatar());
} }
holder.setMessage(concreteNotificaton, statusListener); holder.setMessage(concreteNotificaton, statusListener, bidiFormatter);
holder.setupButtons(notificationActionListener, holder.setupButtons(notificationActionListener,
concreteNotificaton.getAccount().getId(), concreteNotificaton.getAccount().getId(),
concreteNotificaton.getId()); concreteNotificaton.getId());
@ -157,7 +160,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FOLLOW: { case FOLLOW: {
FollowViewHolder holder = (FollowViewHolder) viewHolder; FollowViewHolder holder = (FollowViewHolder) viewHolder;
holder.setMessage(concreteNotificaton.getAccount().getName(), holder.setMessage(concreteNotificaton.getAccount().getName(),
concreteNotificaton.getAccount().getUsername(), concreteNotificaton.getAccount().getAvatar()); concreteNotificaton.getAccount().getUsername(), concreteNotificaton.getAccount().getAvatar(), bidiFormatter);
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId()); holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
break; break;
} }
@ -265,18 +268,19 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
message.setCompoundDrawablesWithIntrinsicBounds(followIcon, null, null, null); message.setCompoundDrawablesWithIntrinsicBounds(followIcon, null, null, null);
} }
void setMessage(String displayName, String username, String avatarUrl) { void setMessage(String displayName, String username, String avatarUrl, BidiFormatter bidiFormatter) {
Context context = message.getContext(); Context context = message.getContext();
String format = context.getString(R.string.notification_follow_format); String format = context.getString(R.string.notification_follow_format);
String wholeMessage = String.format(format, displayName); String wrappedDisplayName = bidiFormatter.unicodeWrap(displayName);
String wholeMessage = String.format(format, wrappedDisplayName);
message.setText(wholeMessage); message.setText(wholeMessage);
format = context.getString(R.string.status_username_format); format = context.getString(R.string.status_username_format);
String wholeUsername = String.format(format, username); String wholeUsername = String.format(format, username);
usernameView.setText(wholeUsername); usernameView.setText(wholeUsername);
displayNameView.setText(displayName); displayNameView.setText(wrappedDisplayName);
if (TextUtils.isEmpty(avatarUrl)) { if (TextUtils.isEmpty(avatarUrl)) {
avatar.setImageResource(R.drawable.avatar_default); avatar.setImageResource(R.drawable.avatar_default);
@ -381,10 +385,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
timestampInfo.setContentDescription(readoutAloud); timestampInfo.setContentDescription(readoutAloud);
} }
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) { void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener, BidiFormatter bidiFormatter) {
this.statusViewData = notificationViewData.getStatusViewData(); this.statusViewData = notificationViewData.getStatusViewData();
String displayName = notificationViewData.getAccount().getName(); String displayName = bidiFormatter.unicodeWrap(notificationViewData.getAccount().getName());
Notification.Type type = notificationViewData.getType(); Notification.Type type = notificationViewData.getType();
Context context = message.getContext(); Context context = message.getContext();

View File

@ -33,6 +33,7 @@ import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.RemoteInput; import android.support.v4.app.RemoteInput;
import android.support.v4.app.TaskStackBuilder; import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v4.text.BidiFormatter;
import android.util.Log; import android.util.Log;
import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.BuildConfig;
@ -119,6 +120,7 @@ public class NotificationHelper {
String rawCurrentNotifications = account.getActiveNotifications(); String rawCurrentNotifications = account.getActiveNotifications();
JSONArray currentNotifications; JSONArray currentNotifications;
BidiFormatter bidiFormatter = BidiFormatter.getInstance();
try { try {
currentNotifications = new JSONArray(rawCurrentNotifications); currentNotifications = new JSONArray(rawCurrentNotifications);
@ -147,7 +149,7 @@ public class NotificationHelper {
notificationId++; notificationId++;
builder.setContentTitle(titleForType(context, body)) builder.setContentTitle(titleForType(context, body, bidiFormatter))
.setContentText(bodyForType(body)); .setContentText(bodyForType(body));
if (body.getType() == Notification.Type.MENTION) { if (body.getType() == Notification.Type.MENTION) {
@ -208,7 +210,7 @@ public class NotificationHelper {
if (currentNotifications.length() != 1) { if (currentNotifications.length() != 1) {
try { try {
String title = context.getString(R.string.notification_title_summary, currentNotifications.length()); String title = context.getString(R.string.notification_title_summary, currentNotifications.length());
String text = joinNames(context, currentNotifications); String text = joinNames(context, currentNotifications, bidiFormatter);
summaryBuilder.setContentTitle(title) summaryBuilder.setContentTitle(title)
.setContentText(text); .setContentText(text);
} catch (JSONException e) { } catch (JSONException e) {
@ -491,38 +493,49 @@ public class NotificationHelper {
} }
} }
private static String wrapItemAt(JSONArray array, int index, BidiFormatter bidiFormatter) throws JSONException {
return bidiFormatter.unicodeWrap(array.get(index).toString());
}
@Nullable @Nullable
private static String joinNames(Context context, JSONArray array) throws JSONException { private static String joinNames(Context context, JSONArray array, BidiFormatter bidiFormatter) throws JSONException {
if (array.length() > 3) { if (array.length() > 3) {
int length = array.length(); int length = array.length();
return String.format(context.getString(R.string.notification_summary_large), return String.format(context.getString(R.string.notification_summary_large),
array.get(length - 1), array.get(length - 2), array.get(length - 3), length - 3); wrapItemAt(array, length - 1, bidiFormatter),
wrapItemAt(array, length - 2, bidiFormatter),
wrapItemAt(array, length - 3, bidiFormatter),
length - 3);
} else if (array.length() == 3) { } else if (array.length() == 3) {
return String.format(context.getString(R.string.notification_summary_medium), return String.format(context.getString(R.string.notification_summary_medium),
array.get(2), array.get(1), array.get(0)); wrapItemAt(array, 2, bidiFormatter),
wrapItemAt(array, 1, bidiFormatter),
wrapItemAt(array, 0, bidiFormatter));
} else if (array.length() == 2) { } else if (array.length() == 2) {
return String.format(context.getString(R.string.notification_summary_small), return String.format(context.getString(R.string.notification_summary_small),
array.get(1), array.get(0)); wrapItemAt(array, 1, bidiFormatter),
wrapItemAt(array, 0, bidiFormatter));
} }
return null; return null;
} }
@Nullable @Nullable
private static String titleForType(Context context, Notification notification) { private static String titleForType(Context context, Notification notification, BidiFormatter bidiFormatter) {
String accountName = bidiFormatter.unicodeWrap(notification.getAccount().getName());
switch (notification.getType()) { switch (notification.getType()) {
case MENTION: case MENTION:
return String.format(context.getString(R.string.notification_mention_format), return String.format(context.getString(R.string.notification_mention_format),
notification.getAccount().getName()); accountName);
case FOLLOW: case FOLLOW:
return String.format(context.getString(R.string.notification_follow_format), return String.format(context.getString(R.string.notification_follow_format),
notification.getAccount().getName()); accountName);
case FAVOURITE: case FAVOURITE:
return String.format(context.getString(R.string.notification_favourite_format), return String.format(context.getString(R.string.notification_favourite_format),
notification.getAccount().getName()); accountName);
case REBLOG: case REBLOG:
return String.format(context.getString(R.string.notification_reblog_format), return String.format(context.getString(R.string.notification_reblog_format),
notification.getAccount().getName()); accountName);
} }
return null; return null;
} }