1 /* 2 * Copyright (C) 2016 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 */ 16 17 package android.widget.cts; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertTrue; 21 import static org.mockito.Matchers.anyInt; 22 import static org.mockito.Matchers.anyString; 23 import static org.mockito.Mockito.spy; 24 import static org.mockito.Mockito.times; 25 import static org.mockito.Mockito.verify; 26 import static org.mockito.Mockito.verifyNoMoreInteractions; 27 import static org.mockito.Mockito.when; 28 29 import android.app.Activity; 30 import android.app.Instrumentation; 31 import android.database.Cursor; 32 import android.database.MatrixCursor; 33 import android.provider.BaseColumns; 34 import android.support.test.uiautomator.By; 35 import android.support.test.uiautomator.UiDevice; 36 import android.text.TextUtils; 37 import android.view.KeyEvent; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.AutoCompleteTextView; 41 import android.widget.CursorAdapter; 42 import android.widget.SearchView; 43 import android.widget.SimpleCursorAdapter; 44 45 import androidx.test.InstrumentationRegistry; 46 import androidx.test.annotation.UiThreadTest; 47 import androidx.test.filters.MediumTest; 48 import androidx.test.rule.ActivityTestRule; 49 import androidx.test.runner.AndroidJUnit4; 50 51 import com.android.compatibility.common.util.CtsKeyEventUtil; 52 import com.android.compatibility.common.util.CtsTouchUtils; 53 import com.android.compatibility.common.util.PollingCheck; 54 import com.android.compatibility.common.util.WidgetTestUtils; 55 56 import org.junit.Before; 57 import org.junit.Rule; 58 import org.junit.Test; 59 import org.junit.runner.RunWith; 60 61 /** 62 * Test {@link SearchView} with {@link Cursor}-backed suggestions adapter. 63 */ 64 @MediumTest 65 @RunWith(AndroidJUnit4.class) 66 public class SearchView_CursorTest { 67 private Instrumentation mInstrumentation; 68 private CtsTouchUtils mCtsTouchUtils; 69 private CtsKeyEventUtil mCtsKeyEventUtil; 70 private Activity mActivity; 71 private SearchView mSearchView; 72 73 private static final String TEXT_COLUMN_NAME = "text"; 74 private String[] mTextContent; 75 76 private CursorAdapter mSuggestionsAdapter; 77 78 // This should be protected to spy an object of this class. 79 protected class MyQueryTextListener implements SearchView.OnQueryTextListener { 80 @Override onQueryTextSubmit(String s)81 public boolean onQueryTextSubmit(String s) { 82 return false; 83 } 84 85 @Override onQueryTextChange(String s)86 public boolean onQueryTextChange(String s) { 87 if (mSuggestionsAdapter == null) { 88 return false; 89 } 90 if (!enoughToFilter()) { 91 return false; 92 } 93 final MatrixCursor c = new MatrixCursor( 94 new String[] { BaseColumns._ID, TEXT_COLUMN_NAME} ); 95 for (int i = 0; i < mTextContent.length; i++) { 96 if (mTextContent[i].toLowerCase().startsWith(s.toLowerCase())) { 97 c.addRow(new Object[]{i, mTextContent[i]}); 98 } 99 } 100 mSuggestionsAdapter.swapCursor(c); 101 return false; 102 } 103 enoughToFilter()104 private boolean enoughToFilter() { 105 final AutoCompleteTextView searchSrcText = findAutoCompleteTextView(mSearchView); 106 return searchSrcText != null && searchSrcText.enoughToFilter(); 107 } 108 findAutoCompleteTextView(final ViewGroup viewGroup)109 private AutoCompleteTextView findAutoCompleteTextView(final ViewGroup viewGroup) { 110 final int count = viewGroup.getChildCount(); 111 for (int index = 0; index < count; index++) { 112 final View view = viewGroup.getChildAt(index); 113 if (view instanceof AutoCompleteTextView) { 114 return (AutoCompleteTextView) view; 115 } 116 if (view instanceof ViewGroup) { 117 final AutoCompleteTextView findView = 118 findAutoCompleteTextView((ViewGroup) view); 119 if (findView != null) { 120 return findView; 121 } 122 } 123 } 124 return null; 125 } 126 } 127 128 // This should be protected to spy an object of this class. 129 protected class MySuggestionListener implements SearchView.OnSuggestionListener { 130 @Override onSuggestionSelect(int position)131 public boolean onSuggestionSelect(int position) { 132 return false; 133 } 134 135 @Override onSuggestionClick(int position)136 public boolean onSuggestionClick(int position) { 137 if (mSuggestionsAdapter != null) { 138 final Cursor cursor = mSuggestionsAdapter.getCursor(); 139 if (cursor != null) { 140 cursor.moveToPosition(position); 141 mSearchView.setQuery(cursor.getString(1), false); 142 } 143 } 144 return true; 145 } 146 } 147 148 @Rule 149 public ActivityTestRule<SearchViewCtsActivity> mActivityRule = 150 new ActivityTestRule<>(SearchViewCtsActivity.class); 151 152 @UiThreadTest 153 @Before setup()154 public void setup() throws Throwable { 155 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 156 mCtsTouchUtils = new CtsTouchUtils(mInstrumentation.getTargetContext()); 157 mCtsKeyEventUtil = new CtsKeyEventUtil(mInstrumentation.getTargetContext()); 158 mActivity = mActivityRule.getActivity(); 159 mSearchView = (SearchView) mActivity.findViewById(R.id.search_view); 160 161 // Local test data for the tests 162 mTextContent = new String[] { "Akon", "Bono", "Ciara", "Dido", "Diplo" }; 163 164 // Use an adapter with our custom layout for each entry. The adapter "maps" 165 // the content of the text column of our cursor to the @id/text1 view in the 166 // layout. 167 mActivityRule.runOnUiThread(() -> { 168 mSuggestionsAdapter = new SimpleCursorAdapter( 169 mActivity, 170 R.layout.searchview_suggestion_item, 171 null, 172 new String[] { TEXT_COLUMN_NAME }, 173 new int[] { android.R.id.text1 }, 174 CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); 175 mSearchView.setSuggestionsAdapter(mSuggestionsAdapter); 176 }); 177 } 178 179 @UiThreadTest 180 @Test testSuggestionFiltering()181 public void testSuggestionFiltering() { 182 final SearchView.OnQueryTextListener mockQueryTextListener = 183 spy(new MyQueryTextListener()); 184 when(mockQueryTextListener.onQueryTextChange(anyString())).thenCallRealMethod(); 185 186 mSearchView.setIconifiedByDefault(false); 187 mSearchView.setOnQueryTextListener(mockQueryTextListener); 188 mSearchView.requestFocus(); 189 190 assertTrue(mSearchView.hasFocus()); 191 assertEquals(mSuggestionsAdapter, mSearchView.getSuggestionsAdapter()); 192 193 mSearchView.setQuery("Bon", false); 194 verify(mockQueryTextListener, times(1)).onQueryTextChange("Bon"); 195 196 mSearchView.setQuery("Di", false); 197 verify(mockQueryTextListener, times(1)).onQueryTextChange("Di"); 198 } 199 200 @Test testSuggestionSelection()201 public void testSuggestionSelection() throws Throwable { 202 final SearchView.OnSuggestionListener mockSuggestionListener = 203 spy(new MySuggestionListener()); 204 when(mockSuggestionListener.onSuggestionClick(anyInt())).thenCallRealMethod(); 205 206 final SearchView.OnQueryTextListener mockQueryTextListener = 207 spy(new MyQueryTextListener()); 208 when(mockQueryTextListener.onQueryTextChange(anyString())).thenCallRealMethod(); 209 210 mActivityRule.runOnUiThread(() -> { 211 mSearchView.setIconifiedByDefault(false); 212 mSearchView.setOnQueryTextListener(mockQueryTextListener); 213 mSearchView.setOnSuggestionListener(mockSuggestionListener); 214 mSearchView.requestFocus(); 215 }); 216 217 assertTrue(mSearchView.hasFocus()); 218 assertEquals(mSuggestionsAdapter, mSearchView.getSuggestionsAdapter()); 219 220 mActivityRule.runOnUiThread(() -> mSearchView.setQuery("Di", false)); 221 PollingCheck.waitFor(() -> { 222 UiDevice uiDevice = UiDevice.getInstance(mInstrumentation); 223 return uiDevice.findObject(By.text("Dido")) != null; 224 }); 225 verify(mockQueryTextListener, times(1)).onQueryTextChange("Di"); 226 227 // Emulate click on the first suggestion - which should be Dido 228 final int suggestionRowHeight = mActivity.getResources().getDimensionPixelSize( 229 R.dimen.search_view_suggestion_row_height); 230 mCtsTouchUtils.emulateTapOnView(mInstrumentation, mActivityRule, mSearchView, 231 mSearchView.getWidth() / 2, mSearchView.getHeight() + suggestionRowHeight / 2); 232 233 // At this point we expect the click on the first suggestion to have activated a sequence 234 // of events that ends up in our suggestion listener that sets the full suggestion text 235 // as the current query. Some parts of this sequence of events are asynchronous, and those 236 // are not "caught" by Instrumentation.waitForIdleSync - which is in general not a very 237 // reliable way to wait for everything to be completed. As such, we are using our own 238 // polling check mechanism to wait until the search view's query is the fully completed 239 // suggestion for Dido. This check will time out and fail after a few seconds if anything 240 // goes wrong during the processing of the emulated tap and the code never gets to our 241 // suggestion listener 242 PollingCheck.waitFor(() -> TextUtils.equals("Dido", mSearchView.getQuery())); 243 244 // Just to be sure, verify that our spy suggestion listener was called 245 verify(mockSuggestionListener, times(1)).onSuggestionClick(0); 246 verifyNoMoreInteractions(mockSuggestionListener); 247 } 248 249 @Test testSuggestionEnterKey()250 public void testSuggestionEnterKey() throws Throwable { 251 final SearchView.OnSuggestionListener mockSuggestionListener = 252 spy(new MySuggestionListener()); 253 when(mockSuggestionListener.onSuggestionClick(anyInt())).thenCallRealMethod(); 254 255 final SearchView.OnQueryTextListener mockQueryTextListener = 256 spy(new MyQueryTextListener()); 257 when(mockQueryTextListener.onQueryTextChange(anyString())).thenCallRealMethod(); 258 259 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mSearchView, () -> { 260 mSearchView.setIconifiedByDefault(false); 261 mSearchView.setOnQueryTextListener(mockQueryTextListener); 262 mSearchView.setOnSuggestionListener(mockSuggestionListener); 263 mSearchView.requestFocus(); 264 mSearchView.setQuery("Di", false); 265 }); 266 267 mInstrumentation.waitForIdleSync(); 268 verify(mockQueryTextListener, times(1)).onQueryTextChange("Di"); 269 270 mCtsKeyEventUtil.sendKeys(mInstrumentation, mSearchView, KeyEvent.KEYCODE_DPAD_DOWN, 271 KeyEvent.KEYCODE_ENTER); 272 273 // Verify that our spy suggestion listener was called. 274 verify(mockSuggestionListener, times(1)).onSuggestionClick(0); 275 276 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mSearchView, () -> { 277 mSearchView.setQuery("Bo", false); 278 }); 279 280 mInstrumentation.waitForIdleSync(); 281 verify(mockQueryTextListener, times(1)).onQueryTextChange("Bo"); 282 283 mCtsKeyEventUtil.sendKeys(mInstrumentation, mSearchView, KeyEvent.KEYCODE_DPAD_DOWN, 284 KeyEvent.KEYCODE_NUMPAD_ENTER); 285 286 // Verify that our spy suggestion listener was called. 287 verify(mockSuggestionListener, times(2)).onSuggestionClick(0); 288 289 verifyNoMoreInteractions(mockSuggestionListener); 290 } 291 } 292