Skip to content

Commit 43a434f

Browse files
Treehugger RobotGerrit Code Review
Treehugger Robot
authored and
Gerrit Code Review
committed
Merge "EmojiCompat will initialize views on non-main thread" into androidx-main
2 parents 228808c + ea326c6 commit 43a434f

File tree

8 files changed

+217
-13
lines changed

8 files changed

+217
-13
lines changed

emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,19 @@
3030
import static org.mockito.Mockito.verify;
3131
import static org.mockito.Mockito.when;
3232

33+
import android.content.Context;
34+
import android.os.Handler;
35+
import android.os.HandlerThread;
3336
import android.text.Spannable;
3437
import android.text.SpannableString;
38+
import android.widget.EditText;
3539
import android.widget.TextView;
3640

3741
import androidx.emoji.text.EmojiCompat;
3842
import androidx.test.ext.junit.runners.AndroidJUnit4;
3943
import androidx.test.filters.LargeTest;
44+
import androidx.test.filters.SdkSuppress;
45+
import androidx.test.platform.app.InstrumentationRegistry;
4046

4147
import org.junit.Before;
4248
import org.junit.Test;
@@ -126,4 +132,31 @@ public void testFilter_withManualLoadStrategy() {
126132
verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
127133
verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
128134
}
135+
136+
@Test
137+
@SdkSuppress(minSdkVersion = 19)
138+
public void initCallback_doesntCrashWhenNotAttached() {
139+
Context context = InstrumentationRegistry.getInstrumentation().getContext();
140+
EditText editText = new EditText(context);
141+
EmojiInputFilter subject = new EmojiInputFilter(editText);
142+
subject.getInitCallback().onInitialized();
143+
}
144+
145+
@Test
146+
@SdkSuppress(minSdkVersion = 29)
147+
public void initCallback_sendsToNonMainHandler_beforeSetText() {
148+
// this is just testing that onInitialized dispatches to editText.getHandler before setText
149+
EditText mockEditText = mock(EditText.class);
150+
HandlerThread thread = new HandlerThread("random thread");
151+
thread.start();
152+
Handler handler = new Handler(thread.getLooper());
153+
thread.quitSafely();
154+
when(mockEditText.getHandler()).thenReturn(handler);
155+
EmojiInputFilter subject = new EmojiInputFilter(mockEditText);
156+
EmojiInputFilter.InitCallbackImpl initCallback =
157+
(EmojiInputFilter.InitCallbackImpl) subject.getInitCallback();
158+
initCallback.onInitialized();
159+
160+
handler.hasCallbacks(initCallback);
161+
}
129162
}

emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,26 @@
2727
import static org.mockito.Mockito.verify;
2828
import static org.mockito.Mockito.when;
2929

30+
import android.content.Context;
31+
import android.os.Handler;
32+
import android.os.HandlerThread;
3033
import android.text.Spannable;
3134
import android.text.SpannableString;
3235
import android.widget.EditText;
3336

3437
import androidx.emoji.text.EmojiCompat;
3538
import androidx.test.ext.junit.runners.AndroidJUnit4;
39+
import androidx.test.filters.SdkSuppress;
3640
import androidx.test.filters.SmallTest;
41+
import androidx.test.platform.app.InstrumentationRegistry;
3742

3843
import org.junit.Before;
3944
import org.junit.Test;
4045
import org.junit.runner.RunWith;
4146

4247
@SmallTest
4348
@RunWith(AndroidJUnit4.class)
49+
@SdkSuppress(minSdkVersion = 19)
4450
public class EmojiTextWatcherTest {
4551

4652
private EmojiTextWatcher mTextWatcher;
@@ -120,4 +126,31 @@ public void testFilter_withManualLoadStrategy() {
120126
verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
121127
verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
122128
}
129+
130+
@Test
131+
public void initCallback_doesntCrashWhenNotAttached() {
132+
Context context = InstrumentationRegistry.getInstrumentation().getContext();
133+
EditText editText = new EditText(context);
134+
EmojiTextWatcher subject = new EmojiTextWatcher(editText);
135+
subject.getInitCallback().onInitialized();
136+
}
137+
138+
@Test
139+
@SdkSuppress(minSdkVersion = 29)
140+
public void initCallback_sendsToNonMainHandler_beforeSetText() {
141+
// this is just testing that onInitialized dispatches to editText.getHandler before setText
142+
EditText mockEditText = mock(EditText.class);
143+
HandlerThread thread = new HandlerThread("random thread");
144+
thread.start();
145+
Handler handler = new Handler(thread.getLooper());
146+
thread.quitSafely();
147+
when(mockEditText.getHandler()).thenReturn(handler);
148+
EmojiTextWatcher subject = new EmojiTextWatcher(mockEditText);
149+
EmojiTextWatcher.InitCallbackImpl initCallback =
150+
(EmojiTextWatcher.InitCallbackImpl) subject.getInitCallback();
151+
initCallback.onInitialized();
152+
153+
handler.hasCallbacks(initCallback);
154+
}
123155
}
156+

emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java

+23-3
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616
package androidx.emoji.widget;
1717

18+
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
1819
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
1920

21+
import android.os.Handler;
2022
import android.text.Selection;
2123
import android.text.Spannable;
2224
import android.text.Spanned;
@@ -88,14 +90,16 @@ public CharSequence filter(final CharSequence source, final int sourceStart,
8890
}
8991
}
9092

91-
private InitCallback getInitCallback() {
93+
@RestrictTo(LIBRARY)
94+
InitCallback getInitCallback() {
9295
if (mInitCallback == null) {
9396
mInitCallback = new InitCallbackImpl(mTextView);
9497
}
9598
return mInitCallback;
9699
}
97100

98-
private static class InitCallbackImpl extends InitCallback {
101+
@RestrictTo(LIBRARY)
102+
static class InitCallbackImpl extends InitCallback implements Runnable {
99103
private final Reference<TextView> mViewRef;
100104

101105
InitCallbackImpl(TextView textView) {
@@ -106,7 +110,23 @@ private static class InitCallbackImpl extends InitCallback {
106110
public void onInitialized() {
107111
super.onInitialized();
108112
final TextView textView = mViewRef.get();
109-
if (textView != null && textView.isAttachedToWindow()) {
113+
if (textView == null) {
114+
return;
115+
}
116+
// we need to move to the actual thread this view is using as main
117+
Handler handler = textView.getHandler();
118+
if (handler != null) {
119+
handler.post(this);
120+
}
121+
}
122+
123+
@Override
124+
public void run() {
125+
final TextView textView = mViewRef.get();
126+
if (textView == null) {
127+
return;
128+
}
129+
if (textView.isAttachedToWindow()) {
110130
final CharSequence result = EmojiCompat.get().process(textView.getText());
111131

112132
final int selectionStart = Selection.getSelectionStart(result);

emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java

+19-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616
package androidx.emoji.widget;
1717

18+
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
1819
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
1920

21+
import android.os.Handler;
2022
import android.text.Editable;
2123
import android.text.Selection;
2224
import android.text.Spannable;
@@ -99,14 +101,16 @@ public void afterTextChanged(Editable s) {
99101
// do nothing
100102
}
101103

102-
private InitCallback getInitCallback() {
104+
@RestrictTo(LIBRARY)
105+
InitCallback getInitCallback() {
103106
if (mInitCallback == null) {
104107
mInitCallback = new InitCallbackImpl(mEditText);
105108
}
106109
return mInitCallback;
107110
}
108111

109-
private static class InitCallbackImpl extends InitCallback {
112+
@RestrictTo(LIBRARY)
113+
static class InitCallbackImpl extends InitCallback implements Runnable {
110114
private final Reference<EditText> mViewRef;
111115

112116
InitCallbackImpl(EditText editText) {
@@ -116,6 +120,19 @@ private static class InitCallbackImpl extends InitCallback {
116120
@Override
117121
public void onInitialized() {
118122
super.onInitialized();
123+
final EditText editText = mViewRef.get();
124+
if (editText == null) {
125+
return;
126+
}
127+
Handler handler = editText.getHandler();
128+
if (handler == null) {
129+
return;
130+
}
131+
handler.post(this);
132+
}
133+
134+
@Override
135+
public void run() {
119136
final EditText editText = mViewRef.get();
120137
if (editText != null && editText.isAttachedToWindow()) {
121138
final Editable text = editText.getEditableText();

emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java

+34-2
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,21 @@
3030
import static org.mockito.Mockito.verifyNoMoreInteractions;
3131
import static org.mockito.Mockito.when;
3232

33+
import android.content.Context;
34+
import android.os.Handler;
35+
import android.os.HandlerThread;
3336
import android.text.InputFilter;
3437
import android.text.Spannable;
3538
import android.text.SpannableString;
39+
import android.widget.EditText;
3640
import android.widget.TextView;
3741

3842
import androidx.emoji2.text.EmojiCompat;
3943
import androidx.emoji2.util.EmojiMatcher;
4044
import androidx.test.ext.junit.runners.AndroidJUnit4;
4145
import androidx.test.filters.LargeTest;
4246
import androidx.test.filters.SdkSuppress;
47+
import androidx.test.platform.app.InstrumentationRegistry;
4348

4449
import org.junit.Before;
4550
import org.junit.Test;
@@ -191,7 +196,7 @@ public void emojiInputFilterAdded_beforeEmojiCompatInit_callsProcess() {
191196

192197
when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
193198
// trigger initialized
194-
captor.getValue().onInitialized();
199+
((Runnable) captor.getValue()).run();
195200

196201
verify(mEmojiCompat).process(eq(testString));
197202
}
@@ -223,7 +228,7 @@ public void emojiInputFilterAdded_beforeEmojiCompatInit_doesntCallSetText_ifSame
223228
when(mEmojiCompat.process(eq(testString))).thenReturn(testString);
224229
when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
225230
// trigger initialized
226-
captor.getValue().onInitialized();
231+
((Runnable) captor.getValue()).run();
227232

228233
// validate interactions don't do anything except check for update
229234
verify(mTextView).getFilters();
@@ -233,4 +238,31 @@ public void emojiInputFilterAdded_beforeEmojiCompatInit_doesntCallSetText_ifSame
233238
// if you add a safe interaction please update test
234239
verifyNoMoreInteractions(mTextView);
235240
}
241+
242+
@Test
243+
@SdkSuppress(minSdkVersion = 19)
244+
public void initCallback_doesntCrashWhenNotAttached() {
245+
Context context = InstrumentationRegistry.getInstrumentation().getContext();
246+
EditText editText = new EditText(context);
247+
EmojiInputFilter subject = new EmojiInputFilter(editText);
248+
subject.getInitCallback().onInitialized();
249+
}
250+
251+
@Test
252+
@SdkSuppress(minSdkVersion = 29)
253+
public void initCallback_sendsToNonMainHandler_beforeSetText() {
254+
// this is just testing that onInitialized dispatches to editText.getHandler before setText
255+
EditText mockEditText = mock(EditText.class);
256+
HandlerThread thread = new HandlerThread("random thread");
257+
thread.start();
258+
Handler handler = new Handler(thread.getLooper());
259+
thread.quitSafely();
260+
when(mockEditText.getHandler()).thenReturn(handler);
261+
EmojiInputFilter subject = new EmojiInputFilter(mockEditText);
262+
EmojiInputFilter.InitCallbackImpl initCallback =
263+
(EmojiInputFilter.InitCallbackImpl) subject.getInitCallback();
264+
initCallback.onInitialized();
265+
266+
handler.hasCallbacks(initCallback);
267+
}
236268
}

emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java

+30
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
import static org.mockito.Mockito.verifyNoMoreInteractions;
2828
import static org.mockito.Mockito.when;
2929

30+
import android.content.Context;
31+
import android.os.Handler;
32+
import android.os.HandlerThread;
3033
import android.text.Spannable;
3134
import android.text.SpannableString;
3235
import android.text.TextUtils;
@@ -37,6 +40,7 @@
3740
import androidx.test.ext.junit.runners.AndroidJUnit4;
3841
import androidx.test.filters.SdkSuppress;
3942
import androidx.test.filters.SmallTest;
43+
import androidx.test.platform.app.InstrumentationRegistry;
4044

4145
import org.junit.Before;
4246
import org.junit.Test;
@@ -149,4 +153,30 @@ public void whenNotConfigured_andNotExpectInitialized_noCalls() {
149153
mTextWatcher.onTextChanged(expected, 0, 0, 1);
150154
assertTrue(TextUtils.equals(expected, "abc"));
151155
}
156+
157+
@Test
158+
public void initCallback_doesntCrashWhenNotAttached() {
159+
Context context = InstrumentationRegistry.getInstrumentation().getContext();
160+
EditText editText = new EditText(context);
161+
EmojiTextWatcher subject = new EmojiTextWatcher(editText, false);
162+
subject.getInitCallback().onInitialized();
163+
}
164+
165+
@Test
166+
@SdkSuppress(minSdkVersion = 29)
167+
public void initCallback_sendsToNonMainHandler_beforeSetText() {
168+
// this is just testing that onInitialized dispatches to editText.getHandler before setText
169+
EditText mockEditText = mock(EditText.class);
170+
HandlerThread thread = new HandlerThread("random thread");
171+
thread.start();
172+
Handler handler = new Handler(thread.getLooper());
173+
thread.quitSafely();
174+
when(mockEditText.getHandler()).thenReturn(handler);
175+
EmojiTextWatcher subject = new EmojiTextWatcher(mockEditText, false);
176+
EmojiTextWatcher.InitCallbackImpl initCallback =
177+
(EmojiTextWatcher.InitCallbackImpl) subject.getInitCallback();
178+
initCallback.onInitialized();
179+
180+
handler.hasCallbacks(initCallback);
181+
}
152182
}

emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package androidx.emoji2.viewsintegration;
1717

18+
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
19+
20+
import android.os.Handler;
1821
import android.text.InputFilter;
1922
import android.text.Selection;
2023
import android.text.Spannable;
@@ -39,7 +42,7 @@
3942
* effects.
4043
*
4144
*/
42-
@RestrictTo(RestrictTo.Scope.LIBRARY)
45+
@RestrictTo(LIBRARY)
4346
@RequiresApi(19)
4447
final class EmojiInputFilter implements android.text.InputFilter {
4548
private final TextView mTextView;
@@ -88,15 +91,17 @@ public CharSequence filter(final CharSequence source, final int sourceStart,
8891
}
8992
}
9093

91-
private InitCallback getInitCallback() {
94+
@RestrictTo(LIBRARY)
95+
InitCallback getInitCallback() {
9296
if (mInitCallback == null) {
9397
mInitCallback = new InitCallbackImpl(mTextView, this);
9498
}
9599
return mInitCallback;
96100
}
97101

102+
@RestrictTo(LIBRARY)
98103
@RequiresApi(19)
99-
private static class InitCallbackImpl extends InitCallback {
104+
static class InitCallbackImpl extends InitCallback implements Runnable {
100105
private final Reference<TextView> mViewRef;
101106
private final Reference<EmojiInputFilter> mEmojiInputFilterReference;
102107

@@ -109,6 +114,19 @@ private static class InitCallbackImpl extends InitCallback {
109114
@Override
110115
public void onInitialized() {
111116
super.onInitialized();
117+
final TextView textView = mViewRef.get();
118+
if (textView == null) {
119+
return;
120+
}
121+
// we need to move to the actual thread this view is using as main
122+
Handler handler = textView.getHandler();
123+
if (handler != null) {
124+
handler.post(this);
125+
}
126+
}
127+
128+
@Override
129+
public void run() {
112130
@Nullable final TextView textView = mViewRef.get();
113131
@Nullable final InputFilter myInputFilter = mEmojiInputFilterReference.get();
114132
if (!isInputFilterCurrentlyRegisteredOnTextView(textView, myInputFilter)) return;

0 commit comments

Comments
 (0)