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