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 com.android.documentsui.queries;
18 
19 import static android.provider.DocumentsContract.QUERY_ARG_DISPLAY_NAME;
20 import static android.provider.DocumentsContract.QUERY_ARG_FILE_SIZE_OVER;
21 import static android.provider.DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER;
22 import static android.provider.DocumentsContract.QUERY_ARG_MIME_TYPES;
23 import static android.provider.DocumentsContract.Root.FLAG_SUPPORTS_SEARCH;
24 
25 import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
26 
27 import static junit.framework.Assert.assertEquals;
28 import static junit.framework.Assert.assertFalse;
29 import static junit.framework.Assert.assertTrue;
30 
31 import static org.mockito.Mockito.mock;
32 import static org.mockito.Mockito.spy;
33 import static org.mockito.Mockito.times;
34 import static org.mockito.Mockito.verify;
35 import static org.mockito.Mockito.when;
36 
37 import android.content.Intent;
38 import android.os.Bundle;
39 import android.os.Handler;
40 import android.provider.DocumentsContract;
41 import android.text.TextUtils;
42 import android.view.View;
43 import android.view.ViewGroup;
44 
45 import androidx.annotation.Nullable;
46 import androidx.test.filters.SmallTest;
47 import androidx.test.runner.AndroidJUnit4;
48 
49 import com.android.documentsui.MetricConsts;
50 import com.android.documentsui.R;
51 import com.android.documentsui.base.DocumentInfo;
52 import com.android.documentsui.base.DocumentStack;
53 import com.android.documentsui.base.EventHandler;
54 import com.android.documentsui.base.RootInfo;
55 import com.android.documentsui.queries.SearchViewManager.SearchManagerListener;
56 import com.android.documentsui.testing.TestEventHandler;
57 import com.android.documentsui.testing.TestHandler;
58 import com.android.documentsui.testing.TestMenu;
59 import com.android.documentsui.testing.TestMenuItem;
60 import com.android.documentsui.testing.TestTimer;
61 
62 import org.junit.Before;
63 import org.junit.Test;
64 import org.junit.runner.RunWith;
65 
66 import java.time.LocalDate;
67 import java.time.ZoneId;
68 import java.util.HashSet;
69 import java.util.Set;
70 import java.util.Timer;
71 import java.util.TimerTask;
72 
73 @RunWith(AndroidJUnit4.class)
74 @SmallTest
75 public final class SearchViewManagerTest {
76 
77     private TestEventHandler<String> mTestEventHandler;
78     private TestTimer mTestTimer;
79     private TestHandler mTestHandler;
80     private TestMenu mTestMenu;
81     private TestMenuItem mSearchMenuItem;
82     private TestableSearchViewManager mSearchViewManager;
83     private SearchChipViewManager mSearchChipViewManager;
84 
85     private boolean mListenerOnSearchChangedCalled;
86 
87     @Before
setUp()88     public void setUp() {
89         mTestEventHandler = new TestEventHandler<>();
90         mTestTimer = new TestTimer();
91         mTestHandler = new TestHandler();
92 
93         final SearchManagerListener searchListener = new SearchManagerListener() {
94             @Override
95             public void onSearchChanged(@Nullable String query) {
96                 mListenerOnSearchChangedCalled = true;
97             }
98 
99             @Override
100             public void onSearchFinished() {
101             }
102 
103             @Override
104             public void onSearchViewChanged(boolean opened) {
105             }
106 
107             @Override
108             public void onSearchChipStateChanged(View v) {
109             }
110 
111             @Override
112             public void onSearchViewFocusChanged(boolean hasFocus) {
113             }
114 
115             @Override
116             public void onSearchViewClearClicked() {
117             }
118         };
119 
120         ViewGroup chipGroup = mock(ViewGroup.class);
121         mSearchChipViewManager = spy(new SearchChipViewManager(chipGroup));
122         mSearchViewManager = new TestableSearchViewManager(searchListener, mTestEventHandler,
123                 mSearchChipViewManager, null /* savedState */, mTestTimer, mTestHandler);
124 
125         mTestMenu = TestMenu.create();
126         mSearchMenuItem = mTestMenu.findItem(R.id.option_menu_search);
127         mSearchViewManager.install(mTestMenu, true, false);
128     }
129 
130     private static class TestableSearchViewManager extends SearchViewManager {
131 
132         private String mHistoryRecorded;
133         private boolean mIsHistoryRecorded;
134 
TestableSearchViewManager( SearchManagerListener listener, EventHandler<String> commandProcessor, SearchChipViewManager chipViewManager, @Nullable Bundle savedState, Timer timer, Handler handler)135         public TestableSearchViewManager(
136                 SearchManagerListener listener,
137                 EventHandler<String> commandProcessor,
138                 SearchChipViewManager chipViewManager,
139                 @Nullable Bundle savedState,
140                 Timer timer,
141                 Handler handler) {
142             super(listener, commandProcessor, chipViewManager, savedState, timer, handler);
143         }
144 
145         @Override
createSearchTask(String newText)146         public TimerTask createSearchTask(String newText) {
147             TimerTask task = super.createSearchTask(newText);
148             TestTimer.Task testTask = new TestTimer.Task(task);
149             return testTask;
150         }
151 
152         @Override
recordHistoryInternal()153         protected void recordHistoryInternal() {
154             mHistoryRecorded = getCurrentSearch();
155             mIsHistoryRecorded = true;
156         }
157 
getRecordedHistory()158         public String getRecordedHistory() {
159             return mHistoryRecorded;
160         }
161 
isHistoryRecorded()162         public boolean isHistoryRecorded() {
163             return mIsHistoryRecorded;
164         }
165     }
166 
fastForwardTo(long timeMs)167     private void fastForwardTo(long timeMs) {
168         mTestTimer.fastForwardTo(timeMs);
169         mTestHandler.dispatchAllMessages();
170     }
171 
172 
173     @Test
testParseQueryContent_ActionIsNotMatched_NotParseQueryContent()174     public void testParseQueryContent_ActionIsNotMatched_NotParseQueryContent() {
175         final String queryString = "query";
176         Intent intent = new Intent();
177         intent.putExtra(Intent.EXTRA_CONTENT_QUERY, queryString);
178 
179         mSearchViewManager.parseQueryContentFromIntent(intent, -1);
180         assertTrue(mSearchViewManager.getQueryContentFromIntent() == null);
181     }
182 
183     @Test
testParseQueryContent_queryContentIsMatched()184     public void testParseQueryContent_queryContentIsMatched() {
185         final String queryString = "query";
186         Intent intent = new Intent();
187         intent.putExtra(Intent.EXTRA_CONTENT_QUERY, queryString);
188 
189         mSearchViewManager.parseQueryContentFromIntent(intent, ACTION_GET_CONTENT);
190         assertEquals(queryString, mSearchViewManager.getQueryContentFromIntent());
191     }
192 
193     @Test
testIsExpanded_ExpandsOnClick()194     public void testIsExpanded_ExpandsOnClick() {
195         mSearchViewManager.onClick(null);
196         assertTrue(mSearchViewManager.isExpanded());
197     }
198 
199     @Test
testIsExpanded_CollapsesOnMenuItemActionCollapse()200     public void testIsExpanded_CollapsesOnMenuItemActionCollapse() {
201         mSearchViewManager.onClick(null);
202         mSearchViewManager.onMenuItemActionCollapse(null);
203         assertFalse(mSearchViewManager.isExpanded());
204     }
205 
206     @Test
testIsSearching_TrueHasCheckedChip()207     public void testIsSearching_TrueHasCheckedChip() throws Exception {
208         mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList();
209         assertTrue(mSearchViewManager.isSearching());
210     }
211 
212     @Test
testIsSearching_FalseOnClick()213     public void testIsSearching_FalseOnClick() throws Exception {
214         mSearchViewManager.onClick(null);
215         assertFalse(mSearchViewManager.isSearching());
216     }
217 
218     @Test
testIsSearching_TrueOnQueryTextSubmit()219     public void testIsSearching_TrueOnQueryTextSubmit() throws Exception {
220         mSearchViewManager.onClick(null);
221         mSearchViewManager.onQueryTextSubmit("query");
222         assertTrue(mSearchViewManager.isSearching());
223     }
224 
225     @Test
testIsSearching_FalseImmediatelyAfterOnQueryTextChange()226     public void testIsSearching_FalseImmediatelyAfterOnQueryTextChange() throws Exception {
227         mSearchViewManager.onClick(null);
228         mSearchViewManager.onQueryTextChange("q");
229         assertFalse(mSearchViewManager.isSearching());
230     }
231 
232     @Test
testIsSearching_TrueAfterOnQueryTextChangeAndWait()233     public void testIsSearching_TrueAfterOnQueryTextChangeAndWait() throws Exception {
234         mSearchViewManager.onClick(null);
235         mSearchViewManager.onQueryTextChange("q");
236         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
237         assertTrue(mSearchViewManager.isSearching());
238     }
239 
240     @Test
testIsSearching_FalseWhenSecondOnQueryTextChangeResetsTimer()241     public void testIsSearching_FalseWhenSecondOnQueryTextChangeResetsTimer() throws Exception {
242         mSearchViewManager.onClick(null);
243         mSearchViewManager.onQueryTextChange("q");
244         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS - 1);
245         mSearchViewManager.onQueryTextChange("qu");
246         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
247         assertFalse(mSearchViewManager.isSearching());
248     }
249 
250     @Test
testIsSearching_TrueAfterSecondOnQueryTextChangeResetsTimer()251     public void testIsSearching_TrueAfterSecondOnQueryTextChangeResetsTimer() throws Exception {
252         mSearchViewManager.onClick(null);
253         mSearchViewManager.onQueryTextChange("q");
254         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS - 1);
255         mSearchViewManager.onQueryTextChange("qu");
256         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS * 2);
257         assertTrue(mSearchViewManager.isSearching());
258     }
259 
260     @Test
testIsSearching_FalseIfSearchCanceled()261     public void testIsSearching_FalseIfSearchCanceled() throws Exception {
262         mSearchViewManager.onClick(null);
263         mSearchViewManager.onQueryTextChange("q");
264         mSearchViewManager.cancelSearch();
265         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
266         assertFalse(mSearchViewManager.isSearching());
267     }
268 
269     @Test
testOnSearchChanged_CalledAfterOnQueryTextSubmit()270     public void testOnSearchChanged_CalledAfterOnQueryTextSubmit() throws Exception {
271         mSearchViewManager.onClick(null);
272         mSearchViewManager.onQueryTextSubmit("q");
273         assertTrue(mListenerOnSearchChangedCalled);
274     }
275 
276     @Test
testOnSearchChanged_NotCalledImmediatelyAfterOnQueryTextChanged()277     public void testOnSearchChanged_NotCalledImmediatelyAfterOnQueryTextChanged() throws Exception {
278         mSearchViewManager.onClick(null);
279         mSearchViewManager.onQueryTextChange("q");
280         assertFalse(mListenerOnSearchChangedCalled);
281     }
282 
283     @Test
testOnSearchChanged_CalledAfterOnQueryTextChangedAndWait()284     public void testOnSearchChanged_CalledAfterOnQueryTextChangedAndWait() throws Exception {
285         mSearchViewManager.onClick(null);
286         mSearchViewManager.onQueryTextChange("q");
287         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
288         assertTrue(mListenerOnSearchChangedCalled);
289     }
290 
291     @Test
testOnSearchChanged_CalledOnlyOnceAfterOnQueryTextSubmit()292     public void testOnSearchChanged_CalledOnlyOnceAfterOnQueryTextSubmit() throws Exception {
293         mSearchViewManager.onClick(null);
294         mSearchViewManager.onQueryTextChange("q");
295         mSearchViewManager.onQueryTextSubmit("q");
296 
297         // Clear the flag to check if it gets set again.
298         mListenerOnSearchChangedCalled = false;
299         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
300         assertFalse(mListenerOnSearchChangedCalled);
301     }
302 
303     @Test
testOnSearchChanged_NotCalledForOnQueryTextSubmitIfSearchAlreadyFinished()304     public void testOnSearchChanged_NotCalledForOnQueryTextSubmitIfSearchAlreadyFinished()
305             throws Exception {
306         mSearchViewManager.onClick(null);
307         mSearchViewManager.onQueryTextChange("q");
308         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
309         // Clear the flag to check if it gets set again.
310         mListenerOnSearchChangedCalled = false;
311         mSearchViewManager.onQueryTextSubmit("q");
312         assertFalse(mListenerOnSearchChangedCalled);
313     }
314 
315     @Test
testHistoryRecorded_recordOnQueryTextSubmit()316     public void testHistoryRecorded_recordOnQueryTextSubmit() {
317         mSearchViewManager.onClick(null);
318         mSearchViewManager.onQueryTextSubmit("q");
319 
320         assertEquals(mSearchViewManager.getCurrentSearch(),
321                 mSearchViewManager.getRecordedHistory());
322     }
323 
324     @Test
testHistoryRecorded_skipWhenNoSearchString()325     public void testHistoryRecorded_skipWhenNoSearchString() {
326         mSearchViewManager.recordHistory();
327 
328         assertFalse(mSearchViewManager.isHistoryRecorded());
329     }
330 
331     @Test
testCheckedChipItems_IsEmptyIfSearchCanceled()332     public void testCheckedChipItems_IsEmptyIfSearchCanceled() throws Exception {
333         mSearchViewManager.onClick(null);
334         mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList();
335         mSearchViewManager.cancelSearch();
336         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
337         assertTrue(!mSearchChipViewManager.hasCheckedItems());
338     }
339 
340     @Test
testBuildQueryArgs_hasSearchString()341     public void testBuildQueryArgs_hasSearchString() throws Exception {
342         final String query = "q";
343         mSearchViewManager.onClick(null);
344         mSearchViewManager.onQueryTextChange("q");
345         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
346 
347         final Bundle queryArgs = mSearchViewManager.buildQueryArgs();
348         assertFalse(queryArgs.isEmpty());
349 
350         final String queryString = queryArgs.getString(DocumentsContract.QUERY_ARG_DISPLAY_NAME);
351         assertEquals(query, queryString);
352     }
353 
354     @Test
testBuildQueryArgs_emptySearchString_expandedSearchWithChips_hasEmptyButNotMissingSearchString()355     public void testBuildQueryArgs_emptySearchString_expandedSearchWithChips_hasEmptyButNotMissingSearchString()
356             throws Exception {
357         mSearchViewManager.onClick(null);
358         mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList();
359         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
360 
361         final String queryString =
362                 mSearchViewManager.buildQueryArgs()
363                         .getString(DocumentsContract.QUERY_ARG_DISPLAY_NAME);
364         assertEquals("", queryString);
365     }
366 
367     @Test
testBuildQueryArgs_emptySearchString_withChipsWithoutExpandedSearch_hasNoSearchString()368     public void testBuildQueryArgs_emptySearchString_withChipsWithoutExpandedSearch_hasNoSearchString()
369             throws Exception {
370         mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList();
371         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
372 
373         assertFalse(mSearchViewManager.buildQueryArgs().containsKey(QUERY_ARG_DISPLAY_NAME));
374     }
375 
376     @Test
testBuildQueryArgs_emptySearchString_expandedSearchWithNoChips_hasNoSearchString()377     public void testBuildQueryArgs_emptySearchString_expandedSearchWithNoChips_hasNoSearchString()
378             throws Exception {
379         mSearchViewManager.onClick(null);
380         fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
381 
382         assertFalse(mSearchViewManager.buildQueryArgs().containsKey(QUERY_ARG_DISPLAY_NAME));
383     }
384 
385     @Test
testBuildQueryArgs_hasMimeType()386     public void testBuildQueryArgs_hasMimeType() throws Exception {
387         mSearchViewManager.onClick(null);
388         mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList();
389 
390         final Bundle queryArgs = mSearchViewManager.buildQueryArgs();
391         assertFalse(queryArgs.isEmpty());
392 
393         final String[] mimeTypes = queryArgs.getStringArray(QUERY_ARG_MIME_TYPES);
394         assertTrue(mimeTypes.length > 0);
395         assertEquals("image/*", mimeTypes[0]);
396     }
397 
398     @Test
testBuildQueryArgs_hasLargeFilesSize()399     public void testBuildQueryArgs_hasLargeFilesSize() throws Exception {
400         mSearchViewManager.onClick(null);
401         mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList();
402 
403         final Bundle queryArgs = mSearchViewManager.buildQueryArgs();
404         assertFalse(queryArgs.isEmpty());
405 
406         final long largeFilesSize = queryArgs.getLong(QUERY_ARG_FILE_SIZE_OVER);
407         assertEquals(10000000L, largeFilesSize);
408     }
409 
410     @Test
testBuildQueryArgs_hasWeekAgoTime()411     public void testBuildQueryArgs_hasWeekAgoTime() throws Exception {
412         mSearchViewManager.onClick(null);
413         mSearchChipViewManager.mCheckedChipItems = getFakeSearchChipDataList();
414 
415         final long startTime = LocalDate.now().minusDays(7).atStartOfDay(ZoneId.systemDefault())
416                 .toInstant().toEpochMilli();
417 
418         final Bundle queryArgs = mSearchViewManager.buildQueryArgs();
419         assertFalse(queryArgs.isEmpty());
420 
421         final long endTime  = LocalDate.now().minusDays(7).atStartOfDay(ZoneId.systemDefault())
422                 .toInstant().toEpochMilli();
423         final long weekAgoTime = queryArgs.getLong(QUERY_ARG_LAST_MODIFIED_AFTER);
424         assertTrue(weekAgoTime == endTime || weekAgoTime == startTime);
425     }
426 
427     @Test
testSupportsMimeTypesSearch_showChips()428     public void testSupportsMimeTypesSearch_showChips() throws Exception {
429         RootInfo root = spy(new RootInfo());
430         when(root.isRecents()).thenReturn(false);
431         root.flags = FLAG_SUPPORTS_SEARCH;
432         root.queryArgs = QUERY_ARG_MIME_TYPES;
433         DocumentStack stack = new DocumentStack(root, new DocumentInfo());
434 
435         mSearchViewManager.showMenu(stack);
436 
437         verify(mSearchChipViewManager, times(1)).setChipsRowVisible(true);
438     }
439 
440     @Test
testNotSupportsMimeTypesSearch_notShowChips()441     public void testNotSupportsMimeTypesSearch_notShowChips() throws Exception {
442         RootInfo root = spy(new RootInfo());
443         when(root.isRecents()).thenReturn(false);
444         root.flags = FLAG_SUPPORTS_SEARCH;
445         root.queryArgs = TextUtils.join("\n",
446                 new String[]{QUERY_ARG_DISPLAY_NAME, QUERY_ARG_FILE_SIZE_OVER,
447                         QUERY_ARG_LAST_MODIFIED_AFTER});
448         DocumentStack stack = new DocumentStack(root, new DocumentInfo());
449 
450         mSearchViewManager.showMenu(stack);
451 
452         verify(mSearchChipViewManager, times(1)).setChipsRowVisible(false);
453     }
454 
455     @Test
testSupportsSearch_showMenu()456     public void testSupportsSearch_showMenu() throws Exception {
457         RootInfo root = spy(new RootInfo());
458         when(root.isRecents()).thenReturn(false);
459         root.flags = FLAG_SUPPORTS_SEARCH;
460         DocumentStack stack = new DocumentStack(root, new DocumentInfo());
461 
462         mSearchViewManager.showMenu(stack);
463 
464         assertTrue(mSearchMenuItem.isVisible());
465     }
466 
467     @Test
testNotSupportsSearch_notShowMenuAndChips()468     public void testNotSupportsSearch_notShowMenuAndChips() throws Exception {
469         RootInfo root = spy(new RootInfo());
470         when(root.isRecents()).thenReturn(false);
471         root.queryArgs = QUERY_ARG_MIME_TYPES;
472         DocumentStack stack = new DocumentStack(root, new DocumentInfo());
473 
474         mSearchViewManager.install(mTestMenu, true, false);
475         mSearchViewManager.showMenu(stack);
476 
477         assertFalse(mSearchMenuItem.isVisible());
478         verify(mSearchChipViewManager, times(1)).setChipsRowVisible(false);
479     }
480 
getFakeSearchChipDataList()481     private static Set<SearchChipData> getFakeSearchChipDataList() {
482         final Set<SearchChipData> chipDataList = new HashSet<>();
483         chipDataList.add(new SearchChipData(MetricConsts.TYPE_CHIP_IMAGES,
484                 0 /* titleRes */, new String[]{"image/*"}));
485         chipDataList.add(new SearchChipData(MetricConsts.TYPE_CHIP_LARGE_FILES,
486                 0 /* titleRes */, new String[]{""}));
487         chipDataList.add(new SearchChipData(MetricConsts.TYPE_CHIP_FROM_THIS_WEEK,
488                 0 /* titleRes */, new String[]{""}));
489         return chipDataList;
490     }
491 }
492