• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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