Android SpannableStringBuilder for multiple Patterns

113 Views Asked by At

I have different screens for different entities which are Products, Rooms, Posts, and, Users. All of them have distinct ID which I use to open the particular screen to try to load the data from the ID parameter passed on the relevant screen/activity via the APIs.

The mechanism I thought for Products,Rooms,and Posts in particular are that on Notifications Screen and alike, I want to send coded or tagged string; like below for Products for example:

You have bid on <p244 24LED Lampshade />

And to match the above I have a regular expression:

<p([0-9]*)\s(.*?)\/>(\s|$)

Here's a sample string:

 String dummyText = "Hi @dan we product test <p1337 product />" +
                        "\n how about <r123 Room name/> is a nice room" +
                        "\n what <t234 this post details more about it/> but you should " +
                        "\n #poprocks woah one #sad";

working OK with single instance but space is still messy at the end though It seems to work fine with single use of the product/post/room, but it messes up with more instances of the coded/tagged string.

But like I stated, with multiple instances like the following test:

 String dummyText = "Hi @dan we product test <p1337 product /> and were more happy with <p53 car/> " +
                        "\n eh <r123 Room name/> is a nice room and <r233 dans house/> sucked while <r123 fun time/> was ok " +
//                        "\n what <t234 this post details more about it/> but you should " +
                        "\n <t234 view this as well/>" +
                        "\n #poprocks woah one #sad";

It messes up:

Multiple instances messes up

Here's my entire code process:

String dummyText = "Hi @dan we product test <p1337 product /> and were more happy with <p53 car/> " +
                        "\n eh <r123 Room name/> is a nice room and <r233 dans house/> sucked while <r123 fun time/> was ok " +
//                        "\n what <t234 this post details more about it/> but you should " +
                        "\n <t234 view this as well/>" +
                        "\n #poprocks woah one #sad";

                List<Pattern> patternsList = new ArrayList<>();
                patternsList.add(Pattern.compile(REGEX_NOTI_ROOMS)); //0
                patternsList.add(Pattern.compile(REGEX_NOTI_PRODUCTS)); //1
                patternsList.add(Pattern.compile(REGEX_NOTI_TWEETS)); //2
                patternsList.add(Pattern.compile(REGEX_NOTI_MENTION)); //3
                patternsList.add(Pattern.compile(REGEX_ARABIC_N_NUM_HASHTAG)); //4
                holder.row_noti_messageTV.setText(makeSpannable(dummyText, patternsList));
                holder.row_noti_messageTV.setMovementMethod(new PkMovementMethod());

Where the relevant regex are:

    public static final String REGEX_NOTI_ROOMS ="<r([0-9]*)\\s(.*?)\\/>(\\s|$)";
    public static final String REGEX_NOTI_PRODUCTS ="<p([0-9]*)\\s(.*?)\\/>(\\s|$)";
    public static final String REGEX_NOTI_TWEETS ="<t([0-9]*)\\s(.*?)\\/>(\\s|$)";
    public static final String REGEX_ARABIC_N_NUM_HASHTAG ="#(\\w*[0-9a-zA-Zء-ي٠-٩]+\\w*[0-9a-zA-Zء-ي٠-٩])";
    public static final String REGEX_NOTI_MENTION ="(?:^|\\s|$|[.])@[\\p{L}0-9_]*";

And my makeSpannable method is:

    public SpannableStringBuilder makeSpannable(String rawText, List<Pattern> listofPatterns) {

//        StringBuffer sb = new StringBuffer();
        SpannableStringBuilder spannable = new SpannableStringBuilder(rawText);

        for(int i=0; i<listofPatterns.size(); i++)
        {
            Matcher matcher = null;
            if(i==0) //init only
                matcher = listofPatterns.get(i).matcher(rawText);
            else
                matcher = listofPatterns.get(i).matcher(spannable);

            while (matcher.find()) {
                showLogMessage("jsonParse", "hit on iteration" + i + " group == " + matcher.group());
                if(i==3 || i == 4)
                {
                    try {
                        String abbr = matcher.group();
                        showLogMessage("jsonParse", "loop[3 | 4 are normal] == " + i + " group(0) == " + abbr);

                        int start = matcher.start();
                        int end = matcher.end();

                        NotificationSpan ourSpan = new NotificationSpan(Color.BLUE, Color.RED, Color.TRANSPARENT, new IdTitleStore(abbr, abbr, abbr), NotificationAdapter.this);
                        spannable.setSpan(ourSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                else if(i<=2) {
                    try {
                        String orgGroup = matcher.group();
                        // get the match
                        String abbr = matcher.group(2);
                        showLogMessage("jsonParse", "loop == " + i + " group2 == " + abbr);

                        int startPoint = matcher.start();
                        int endPoint = matcher.end();

                        NotificationSpan ourSpan = new NotificationSpan(Color.RED, Color.BLUE, Color.TRANSPARENT, new IdTitleStore(matcher.group(1), abbr, orgGroup), NotificationAdapter.this);
                        Spannable spanText = new SpannableString(abbr);
                        spanText.setSpan(
                                ourSpan, 0, abbr.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                        );

                        spannable.replace(startPoint, endPoint, spanText);


                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }

            }
        }

        return spannable;
    }

Note: NotificationSpan is a custom span I use to store the group(1) as it has the ID and from group(0) I can deduce on click whether it may be a Post, Product, or a Room entity to open it.

Any feedback shall be appreciated, even if it can point me towards the right direction. Like if my approach in itself is fundamentally wrong or something, please do let me know.

EDIT: Someone pointed out at comments to remove the redundant (\s|$) from the Regular Expressions, and it did seemingly nothing

Logs for the dummyString I tested:

String dummyText = "Hi @dan we product test <p1337 product /> and were more happy with <p53 car/> " +
                        "\n eh <r123 Room name/> is a nice room and <r233 dans house/> sucked while <r123 fun time/> was ok " +
                        "\n what <t233 this post details more about it/> but you should " +
                        "\n <t234 view this as well/>" +
                        "\n #poprocks woah one #sad";

E/jsonParse: hit on iteration0 group(0) == <r123 Room name/>
E/jsonParse: hit on iteration0 group(0) == <r233 dans house/>
E/jsonParse: hit on iteration0 group(0) == <r123 fun time/>
E/jsonParse: hit on iteration1 group(0) == <p1337 product />
E/jsonParse: hit on iteration1 group(0) == <p53 car/>
E/jsonParse: hit on iteration2 group(0) == <t234 view this as well/>
E/jsonParse: hit on iteration3 group(0) ==  @dan
E/jsonParse: hit on iteration4 group(0) == #poprocks
E/jsonParse: hit on iteration4 group(0) == #sad

It is also weird that the first instance is not detected by the matcher at all. i.e

"\n what <t234 this post details more about it/> but you should "

It may entirely be possible that something is wrong with the logic itself of the Multiple Patterns being matched inside loops, but I really can't seem to figure it out. Appreciate the comment though!

EDIT EDIT*: I think I finally understand what the problem was/is. Adding dot after the replace method it gave me two suggestions, notify() and notifyAll(), which got me wondering this is indeed multi-thread based operation (sort of obvious to others but yeah!). So the multiple loop was in fact the issue.

Since replacing a call updates the span itself, the inner loop (while mathcer.find()) did not have the latest span on the matcher, it had different/previous one, which would've worked fine if there were only one instance found, but since in case of multiple the start and end were way off and hence some unintended stuff was happening. Followings the updated code, I did keep the (\s|$) just in case a Product/Post/Room is the end of the string so it does match and not remain blank.

public SpannableStringBuilder makeSpannable(String rawText, List<Pattern> listofPatterns) {

        SpannableStringBuilder spannable = new SpannableStringBuilder(rawText);

        Matcher matcher = null;
        for(int i=0; i<listofPatterns.size(); i++)
        {
            matcher = listofPatterns.get(i).matcher(spannable.toString());

            while (matcher.find()) {
                showLogMessage("jsonParse", "hit on iteration" + i + " group == " + matcher.group());
                if(i<=2) {
                    try {
                        String orgGroup = matcher.group();
                        // get the match
                        String abbr = matcher.group(2);
                        showLogMessage("jsonParse", "span txt of iteration " + i + " going to be group2 == " + abbr);

                        int startPoint = matcher.start();
                        int endPoint = matcher.end();

                        NotificationSpan ourSpan = new NotificationSpan(Color.RED, Color.BLUE, Color.TRANSPARENT, new IdTitleStore(matcher.group(1), abbr, orgGroup), NotificationAdapter.this);
                        Spannable spanText = new SpannableString(abbr);
                        spanText.setSpan(
                                ourSpan, 0, abbr.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                        );

                        spannable.replace(startPoint, endPoint-1, spanText);
                        matcher = listofPatterns.get(i).matcher(spannable);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                else {
                    try {
                        String abbr = matcher.group();
                        showLogMessage("jsonParse", "span txt of iteration " + i + " going to be group(0) == " + abbr);

                        int start = matcher.start();
                        int end = matcher.end();

                        NotificationSpan ourSpan = new NotificationSpan(Color.BLUE, Color.RED, Color.TRANSPARENT, new IdTitleStore(abbr, abbr, abbr), NotificationAdapter.this);
                        spannable.setSpan(ourSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return spannable;

    }

Additional note: I think it'd be better if I explain why I've divided the loop into two parts to begin-with as well. The reason is simple, first 3 of my REGEX I 100% know for sure have group(2) elements, which I have implemented the click ID mechanism of. The last two are normal hashtag and mention ones which don't have group(2).

Here's the final working look, hope I don't face any performance issues:

Code finally working after a lot of trial and suffering and errors.

0

There are 0 best solutions below