Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 92f97a88 authored by Geoffrey Pitsch's avatar Geoffrey Pitsch
Browse files

Show search results automatically populate after text change

Search kicks off after .75 seconds without typing.

Fixes: 68841263
Test: docsui unit tests
Change-Id: I682ff594784ef9d5a9d296a0b6ce4cac6edde03f
parent b28949e8
Loading
Loading
Loading
Loading
+79 −7
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@ import static com.android.documentsui.base.Shared.DEBUG;

import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.DocumentsContract.Root;
import android.text.TextUtils;
import android.util.Log;
@@ -38,6 +40,11 @@ import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.util.Timer;
import java.util.TimerTask;

/**
 * Manages searching UI behavior.
@@ -48,9 +55,19 @@ public class SearchViewManager implements

    private static final String TAG = "SearchManager";

    // How long we wait after the user finishes typing before kicking off a search.
    public static final int SEARCH_DELAY_MS = 750;

    private final SearchManagerListener mListener;
    private final EventHandler<String> mCommandProcessor;

    private final Timer mTimer;
    private final Handler mUiHandler;

    private final Object mSearchLock;
    @GuardedBy("mSearchLock")
    private @Nullable Runnable mQueuedSearchRunnable;
    @GuardedBy("mSearchLock")
    private @Nullable TimerTask mQueuedSearchTask;
    private @Nullable String mCurrentSearch;
    private boolean mSearchExpanded;
    private boolean mIgnoreNextClose;
@@ -64,12 +81,25 @@ public class SearchViewManager implements
            SearchManagerListener listener,
            EventHandler<String> commandProcessor,
            @Nullable Bundle savedState) {
        this(listener, commandProcessor, savedState, new Timer(),
                new Handler(Looper.getMainLooper()));
    }

    @VisibleForTesting
    protected SearchViewManager(
            SearchManagerListener listener,
            EventHandler<String> commandProcessor,
            @Nullable Bundle savedState,
            Timer timer,
            Handler handler) {
        assert (listener != null);
        assert (commandProcessor != null);

        mSearchLock = new Object();
        mListener = listener;
        mCommandProcessor = commandProcessor;
        mTimer = timer;
        mUiHandler = handler;
        mCurrentSearch = savedState != null ? savedState.getString(Shared.EXTRA_QUERY) : null;
    }

@@ -169,6 +199,7 @@ public class SearchViewManager implements
     */
    public boolean cancelSearch() {
        if (isExpanded() || isSearching()) {
            cancelQueuedSearch();
            // If the query string is not empty search view won't get iconified
            mSearchView.setQuery("", false);

@@ -183,6 +214,17 @@ public class SearchViewManager implements
        return false;
    }

    private void cancelQueuedSearch() {
        synchronized (mSearchLock) {
            if (mQueuedSearchTask != null) {
                mQueuedSearchTask.cancel();
            }
            mQueuedSearchTask = null;
            mUiHandler.removeCallbacks(mQueuedSearchRunnable);
            mQueuedSearchRunnable = null;
        }
    }

    /**
     * Sets search view into the searching state. Used to restore state after device orientation
     * change.
@@ -261,10 +303,14 @@ public class SearchViewManager implements
        if (mCommandProcessor.accept(query)) {
            mSearchView.setQuery("", false);
        } else {
            cancelQueuedSearch();
            // Don't kick off a search if we've already finished it.
            if (mCurrentSearch != query) {
                mCurrentSearch = query;
            mSearchView.clearFocus();
                mListener.onSearchChanged(mCurrentSearch);
            }
            mSearchView.clearFocus();
        }

        return true;
    }
@@ -283,9 +329,35 @@ public class SearchViewManager implements
        }
    }

    @VisibleForTesting
    protected TimerTask createSearchTask(String newText) {
        return new TimerTask() {
            @Override
            public void run() {
                // Do the actual work on the main looper.
                synchronized (mSearchLock) {
                    mQueuedSearchRunnable = () -> {
                        mCurrentSearch = newText;
                        if (mCurrentSearch != null && mCurrentSearch.isEmpty()) {
                            mCurrentSearch = null;
                        }
                        mListener.onSearchChanged(mCurrentSearch);
                    };
                    mUiHandler.post(mQueuedSearchRunnable);
                }
            }
        };
    }

    @Override
    public boolean onQueryTextChange(String newText) {
        return false;
        cancelQueuedSearch();
        synchronized (mSearchLock) {
            mQueuedSearchTask = createSearchTask(newText);

            mTimer.schedule(mQueuedSearchTask, SEARCH_DELAY_MS);
        }
        return true;
    }

    @Override
+6 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.documentsui.testing;

import android.util.SparseArray;
import android.view.Menu;
import android.widget.SearchView;

import com.android.documentsui.R;

@@ -88,6 +89,11 @@ public abstract class TestMenu implements Menu {
        for (int id : ids) {
            TestMenuItem item = TestMenuItem.create(id);
            menu.addMenuItem(id, item);

            // Used by SearchViewManager
            if (id == R.id.option_menu_search) {
                item.setActionView(Mockito.mock(SearchView.class));
            }
        }
        return menu;
    }
+13 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue;

import android.annotation.StringRes;
import android.view.MenuItem;
import android.view.View;

import org.mockito.Mockito;

@@ -37,6 +38,7 @@ public abstract class TestMenuItem implements MenuItem {

    boolean enabled;
    boolean visible;
    View actionView;
    @StringRes int title;

    public static TestMenuItem create(int id) {
@@ -83,6 +85,17 @@ public abstract class TestMenuItem implements MenuItem {
        return this.enabled;
    }

    @Override
    final public MenuItem setActionView(View actionView) {
        this.actionView = actionView;
        return this;
    }

    @Override
    final public View getActionView() {
        return this.actionView;
    }

    public void assertEnabled() {
        assertTrue(this.enabled);
    }
+6 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.documentsui.testing;

import java.lang.IllegalStateException;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
@@ -28,6 +29,7 @@ import java.util.TimerTask;
 */
public class TestTimer extends Timer {

    private boolean mIsCancelled;
    private long mNow = 0;

    private final LinkedList<Task> mTaskList = new LinkedList<>();
@@ -64,6 +66,7 @@ public class TestTimer extends Timer {

    @Override
    public void cancel() {
        mIsCancelled = true;
        mTaskList.clear();
    }

@@ -114,6 +117,9 @@ public class TestTimer extends Timer {
    }

    public void scheduleAtTime(TimerTask task, long executeTime) {
        if (mIsCancelled) {
            throw new IllegalStateException("Timer already cancelled.");
        }
        Task testTimerTask = (Task) task;
        testTimerTask.mExecuteTime = executeTime;

+223 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.documentsui.queries;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;

import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Handler;

import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import com.android.documentsui.R;
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.queries.SearchViewManager.SearchManagerListener;
import com.android.documentsui.queries.SearchViewManager;
import com.android.documentsui.testing.TestEventHandler;
import com.android.documentsui.testing.TestHandler;
import com.android.documentsui.testing.TestMenu;
import com.android.documentsui.testing.TestTimer;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Timer;
import java.util.TimerTask;

@RunWith(AndroidJUnit4.class)
@SmallTest
public final class SearchViewManagerTest {

    private TestEventHandler<String> mTestEventHandler;
    private TestTimer mTestTimer;
    private TestHandler mTestHandler;
    private SearchViewManager mSearchViewManager;

    private boolean mListenerOnSearchChangedCalled;

    @Before
    public void setUp() {
        mTestEventHandler = new TestEventHandler<>();
        mTestTimer = new TestTimer();
        mTestHandler = new TestHandler();

        final SearchManagerListener searchListener = new SearchManagerListener() {
            @Override
            public void onSearchChanged(@Nullable String query) {
                mListenerOnSearchChangedCalled = true;
            }
            @Override
            public void onSearchFinished() {}
            @Override
            public void onSearchViewChanged(boolean opened) {}
        };

        mSearchViewManager = new TestableSearchViewManager(
                searchListener, mTestEventHandler, null, mTestTimer, mTestHandler);

        final TestMenu testMenu = TestMenu.create();
        mSearchViewManager.install(testMenu, true);
    }

    private static class TestableSearchViewManager extends SearchViewManager {
        public TestableSearchViewManager(
                SearchManagerListener listener,
                EventHandler<String> commandProcessor,
                @Nullable Bundle savedState,
                Timer timer,
                Handler handler) {
            super(listener, commandProcessor, savedState, timer, handler);
        }

        @Override
        public TimerTask createSearchTask(String newText) {
            TimerTask task = super.createSearchTask(newText);
            TestTimer.Task testTask = new TestTimer.Task(task);
            return testTask;
        }
    }

    private void fastForwardTo(long timeMs) {
        mTestTimer.fastForwardTo(timeMs);
        mTestHandler.dispatchAllMessages();
    }

    @Test
    public void testIsExpanded_ExpandsOnClick() {
        mSearchViewManager.onClick(null);
        assertTrue(mSearchViewManager.isExpanded());
    }

    @Test
    public void testIsExpanded_CollapsesOnMenuItemActionCollapse() {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onMenuItemActionCollapse(null);
        assertFalse(mSearchViewManager.isExpanded());
    }

    @Test
    public void testIsSearching_FalseOnClick() throws Exception {
        mSearchViewManager.onClick(null);
        assertFalse(mSearchViewManager.isSearching());
    }

    @Test
    public void testIsSearching_TrueOnQueryTextSubmit() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextSubmit("query");
        assertTrue(mSearchViewManager.isSearching());
    }

    @Test
    public void testIsSearching_FalseImmediatelyAfterOnQueryTextChange() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        assertFalse(mSearchViewManager.isSearching());
    }

    @Test
    public void testIsSearching_TrueAfterOnQueryTextChangeAndWait() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
        assertTrue(mSearchViewManager.isSearching());
    }

    @Test
    public void testIsSearching_FalseWhenSecondOnQueryTextChangeResetsTimer() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS - 1);
        mSearchViewManager.onQueryTextChange("qu");
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
        assertFalse(mSearchViewManager.isSearching());
    }

    @Test
    public void testIsSearching_TrueAfterSecondOnQueryTextChangeResetsTimer() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS - 1);
        mSearchViewManager.onQueryTextChange("qu");
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS * 2);
        assertTrue(mSearchViewManager.isSearching());
    }

    @Test
    public void testIsSearching_FalseIfSearchCanceled() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        mSearchViewManager.cancelSearch();
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
        assertFalse(mSearchViewManager.isSearching());
    }

    @Test
    public void testOnSearchChanged_CalledAfterOnQueryTextSubmit() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextSubmit("q");
        assertTrue(mListenerOnSearchChangedCalled);
    }

    @Test
    public void testOnSearchChanged_NotCalledImmediatelyAfterOnQueryTextChanged() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        assertFalse(mListenerOnSearchChangedCalled);
    }

    @Test
    public void testOnSearchChanged_CalledAfterOnQueryTextChangedAndWait() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
        assertTrue(mListenerOnSearchChangedCalled);
    }

    @Test
    public void testOnSearchChanged_CalledOnlyOnceAfterOnQueryTextSubmit() throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        mSearchViewManager.onQueryTextSubmit("q");

        // Clear the flag to check if it gets set again.
        mListenerOnSearchChangedCalled = false;
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
        assertFalse(mListenerOnSearchChangedCalled);
    }

    @Test
    public void testOnSearchChanged_NotCalledForOnQueryTextSubmitIfSearchAlreadyFinished()
            throws Exception {
        mSearchViewManager.onClick(null);
        mSearchViewManager.onQueryTextChange("q");
        fastForwardTo(SearchViewManager.SEARCH_DELAY_MS);
        // Clear the flag to check if it gets set again.
        mListenerOnSearchChangedCalled = false;
        mSearchViewManager.onQueryTextSubmit("q");
        assertFalse(mListenerOnSearchChangedCalled);
    }
}