Fix crash in time zone picker due to race condition on view updates

- Can't reproduce the race condition with manual test, probably the view
  updates are fast enough that only monkey test can reproduce the issue.
- Reproduced a similar stacktrace and IndexOutOfBoundsException with
  Robolectric test by assuming that the race condition happens after
  text filtering and view updates. Try to fix the bug with this assumption
- The fix is to bind the data (data position in adapter) with ViewHolder.

Bug: 75322108
Test: m RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.datetime.timezone
Change-Id: Ie5d932bce30590b8067e042c3380911c9608872f
This commit is contained in:
Victor Chang
2018-03-24 18:07:50 +00:00
parent d7ea524e81
commit 201c629fcc
5 changed files with 138 additions and 43 deletions

View File

@@ -16,7 +16,20 @@
package com.android.settings.datetime.timezone;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.app.Fragment;
import android.widget.Filter;
import android.widget.LinearLayout;
import com.android.settings.datetime.timezone.BaseTimeZoneAdapter.AdapterItem;
import com.android.settings.datetime.timezone.BaseTimeZoneAdapter.ItemViewHolder;
import com.android.settings.datetime.timezone.RegionSearchPicker.RegionItem;
import com.android.settings.datetime.timezone.model.TimeZoneData;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
@@ -24,17 +37,23 @@ import libcore.util.CountryZonesFinder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(shadows = {
RegionSearchPickerTest.ShadowBaseTimeZonePicker.class,
RegionSearchPickerTest.ShadowFragment.class,
}
)
public class RegionSearchPickerTest {
@Test
@@ -44,16 +63,90 @@ public class RegionSearchPickerTest {
CountryZonesFinder finder = mock(CountryZonesFinder.class);
when(finder.lookupAllCountryIsoCodes()).thenReturn(regionList);
RegionSearchPicker picker = new RegionSearchPicker() {
@Override
protected Locale getLocale() {
return Locale.US;
}
};
RegionSearchPicker picker = new RegionSearchPicker();
BaseTimeZoneAdapter adapter = picker.createAdapter(new TimeZoneData(finder));
assertEquals(1, adapter.getItemCount());
AdapterItem item = adapter.getItem(0);
assertEquals("United States", item.getTitle().toString());
assertThat(Arrays.asList(item.getSearchKeys())).contains("United States");
}
// Test RegionSearchPicker does not crash due to the wrong assumption that no view is clicked
// before all views are updated and after internal data structure is updated for text filtering.
// This test mocks the text filtering event and emit click event immediately
// http://b/75322108
@Test
public void clickItemView_duringRegionSearch_shouldNotCrash() {
List regionList = new ArrayList();
regionList.add("US");
CountryZonesFinder finder = mock(CountryZonesFinder.class);
when(finder.lookupAllCountryIsoCodes()).thenReturn(regionList);
// Prepare the picker and adapter
RegionSearchPicker picker = new RegionSearchPicker();
BaseTimeZoneAdapter<RegionItem> adapter = picker.createAdapter(new TimeZoneData(finder));
// Prepare and bind a new ItemViewHolder with United States
ItemViewHolder viewHolder = adapter.onCreateViewHolder(
new LinearLayout(RuntimeEnvironment.application), 0);
adapter.onBindViewHolder(viewHolder, 0);
assertEquals(1, adapter.getItemCount());
// Pretend to search for a unknown region and no result is found.
FilterWrapper filterWrapper = new FilterWrapper(adapter.getFilter());
filterWrapper.publishEmptyResult("Unknown region 1");
// Assert that the adapter should have been updated with no item
assertEquals(0, adapter.getItemCount());
viewHolder.itemView.performClick(); // This should not crash
}
// FilterResults is a protected inner class. Use FilterWrapper to create an empty FilterResults
// instance.
private static class FilterWrapper extends Filter {
private final BaseTimeZoneAdapter.ArrayFilter mFilter;
private FilterWrapper(BaseTimeZoneAdapter.ArrayFilter filter) {
mFilter = filter;
}
@Override
protected FilterResults performFiltering(CharSequence charSequence) {
return null;
}
private void publishEmptyResult(CharSequence charSequence) {
FilterResults filterResults = new FilterResults();
filterResults.count = 0;
filterResults.values = new ArrayList<>();
publishResults(charSequence, filterResults);
}
@Override
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
mFilter.publishResults(charSequence, filterResults);
}
}
// Robolectric can't start android.app.Fragment with support library v4 resources. Pretend
// the fragment has started, and provide the objects in context here.
@Implements(BaseTimeZonePicker.class)
public static class ShadowBaseTimeZonePicker extends ShadowFragment {
@Implementation
protected Locale getLocale() {
return Locale.US;
}
}
@Implements(Fragment.class)
public static class ShadowFragment {
private Activity mActivity = Robolectric.setupActivity(Activity.class);
@Implementation
public final Activity getActivity() {
return mActivity;
}
}
}