1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package android.support.text.emoji.util; 17 18import static org.mockito.Matchers.argThat; 19 20import android.support.text.emoji.EmojiSpan; 21import android.text.Spanned; 22import android.text.TextUtils; 23 24import org.hamcrest.BaseMatcher; 25import org.hamcrest.Description; 26import org.hamcrest.Matcher; 27import org.hamcrest.TypeSafeMatcher; 28 29/** 30 * Utility class that includes matchers specific to emojis and EmojiSpans. 31 */ 32public class EmojiMatcher { 33 34 public static Matcher<CharSequence> hasEmojiAt(final int id, final int start, 35 final int end) { 36 return new EmojiResourceMatcher(id, start, end); 37 } 38 39 public static Matcher<CharSequence> hasEmojiAt(final Emoji.EmojiMapping emojiMapping, 40 final int start, final int end) { 41 return new EmojiResourceMatcher(emojiMapping.id(), start, end); 42 } 43 44 public static Matcher<CharSequence> hasEmojiAt(final int start, final int end) { 45 return new EmojiResourceMatcher(-1, start, end); 46 } 47 48 public static Matcher<CharSequence> hasEmoji(final int id) { 49 return new EmojiResourceMatcher(id, -1, -1); 50 } 51 52 public static Matcher<CharSequence> hasEmoji(final Emoji.EmojiMapping emojiMapping) { 53 return new EmojiResourceMatcher(emojiMapping.id(), -1, -1); 54 } 55 56 public static Matcher<CharSequence> hasEmoji() { 57 return new EmojiSpanMatcher(); 58 } 59 60 public static Matcher<CharSequence> hasEmojiCount(final int count) { 61 return new EmojiCountMatcher(count); 62 } 63 64 public static <T extends CharSequence> T sameCharSequence(final T expected) { 65 return argThat(new BaseMatcher<T>() { 66 @Override 67 public boolean matches(Object o) { 68 if (o instanceof CharSequence && expected.getClass() == o.getClass()) { 69 return TextUtils.equals(expected, (CharSequence) o); 70 } 71 return false; 72 } 73 74 @Override 75 public void describeTo(Description description) { 76 description.appendText("doesn't match " + expected); 77 } 78 }); 79 } 80 81 private static class EmojiSpanMatcher extends TypeSafeMatcher<CharSequence> { 82 83 private EmojiSpan[] mSpans; 84 85 EmojiSpanMatcher() { 86 } 87 88 @Override 89 public void describeTo(Description description) { 90 description.appendText("should have EmojiSpans"); 91 } 92 93 @Override 94 protected void describeMismatchSafely(final CharSequence charSequence, 95 Description mismatchDescription) { 96 mismatchDescription.appendText(" has no EmojiSpans"); 97 } 98 99 @Override 100 protected boolean matchesSafely(final CharSequence charSequence) { 101 if (charSequence == null) return false; 102 if (!(charSequence instanceof Spanned)) return false; 103 mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class); 104 return mSpans.length != 0; 105 } 106 } 107 108 private static class EmojiCountMatcher extends TypeSafeMatcher<CharSequence> { 109 110 private final int mCount; 111 private EmojiSpan[] mSpans; 112 113 EmojiCountMatcher(final int count) { 114 mCount = count; 115 } 116 117 @Override 118 public void describeTo(Description description) { 119 description.appendText("should have ").appendValue(mCount).appendText(" EmojiSpans"); 120 } 121 122 @Override 123 protected void describeMismatchSafely(final CharSequence charSequence, 124 Description mismatchDescription) { 125 mismatchDescription.appendText(" has "); 126 if (mSpans == null) { 127 mismatchDescription.appendValue("no"); 128 } else { 129 mismatchDescription.appendValue(mSpans.length); 130 } 131 132 mismatchDescription.appendText(" EmojiSpans"); 133 } 134 135 @Override 136 protected boolean matchesSafely(final CharSequence charSequence) { 137 if (charSequence == null) return false; 138 if (!(charSequence instanceof Spanned)) return false; 139 mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class); 140 return mSpans.length == mCount; 141 } 142 } 143 144 private static class EmojiResourceMatcher extends TypeSafeMatcher<CharSequence> { 145 private static final int ERR_NONE = 0; 146 private static final int ERR_SPANNABLE_NULL = 1; 147 private static final int ERR_NO_SPANS = 2; 148 private static final int ERR_WRONG_INDEX = 3; 149 private final int mResId; 150 private final int mStart; 151 private final int mEnd; 152 private int mError = ERR_NONE; 153 private int mActualStart = -1; 154 private int mActualEnd = -1; 155 156 EmojiResourceMatcher(int resId, int start, int end) { 157 mResId = resId; 158 mStart = start; 159 mEnd = end; 160 } 161 162 @Override 163 public void describeTo(final Description description) { 164 if (mResId == -1) { 165 description.appendText("should have EmojiSpan at ") 166 .appendValue("[" + mStart + "," + mEnd + "]"); 167 } else if (mStart == -1 && mEnd == -1) { 168 description.appendText("should have EmojiSpan with resource id ") 169 .appendValue(Integer.toHexString(mResId)); 170 } else { 171 description.appendText("should have EmojiSpan with resource id ") 172 .appendValue(Integer.toHexString(mResId)) 173 .appendText(" at ") 174 .appendValue("[" + mStart + "," + mEnd + "]"); 175 } 176 } 177 178 @Override 179 protected void describeMismatchSafely(final CharSequence charSequence, 180 Description mismatchDescription) { 181 int offset = 0; 182 mismatchDescription.appendText("["); 183 while (offset < charSequence.length()) { 184 int codepoint = Character.codePointAt(charSequence, offset); 185 mismatchDescription.appendText(Integer.toHexString(codepoint)); 186 offset += Character.charCount(codepoint); 187 if (offset < charSequence.length()) { 188 mismatchDescription.appendText(","); 189 } 190 } 191 mismatchDescription.appendText("]"); 192 193 switch (mError) { 194 case ERR_NO_SPANS: 195 mismatchDescription.appendText(" had no spans"); 196 break; 197 case ERR_SPANNABLE_NULL: 198 mismatchDescription.appendText(" was null"); 199 break; 200 case ERR_WRONG_INDEX: 201 mismatchDescription.appendText(" had Emoji at ") 202 .appendValue("[" + mActualStart + "," + mActualEnd + "]"); 203 break; 204 default: 205 mismatchDescription.appendText(" does not have an EmojiSpan with given " 206 + "resource id "); 207 } 208 } 209 210 @Override 211 protected boolean matchesSafely(final CharSequence charSequence) { 212 if (charSequence == null) { 213 mError = ERR_SPANNABLE_NULL; 214 return false; 215 } 216 217 if (!(charSequence instanceof Spanned)) { 218 mError = ERR_NO_SPANS; 219 return false; 220 } 221 222 Spanned spanned = (Spanned) charSequence; 223 final EmojiSpan[] spans = spanned.getSpans(0, charSequence.length(), EmojiSpan.class); 224 225 if (spans.length == 0) { 226 mError = ERR_NO_SPANS; 227 return false; 228 } 229 230 if (mStart == -1 && mEnd == -1) { 231 for (int index = 0; index < spans.length; index++) { 232 if (mResId == spans[index].getId()) { 233 return true; 234 } 235 } 236 return false; 237 } else { 238 for (int index = 0; index < spans.length; index++) { 239 if (mResId == -1 || mResId == spans[index].getId()) { 240 mActualStart = spanned.getSpanStart(spans[index]); 241 mActualEnd = spanned.getSpanEnd(spans[index]); 242 if (mActualStart == mStart && mActualEnd == mEnd) { 243 return true; 244 } 245 } 246 } 247 248 if (mActualStart != -1 && mActualEnd != -1) { 249 mError = ERR_WRONG_INDEX; 250 } 251 252 return false; 253 } 254 } 255 } 256} 257