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