blob: bccf03c6a1d8e04cd33e8cdd69a79492261aa80e [file] [log] [blame]
/*
* Copyright (C) 2020 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 android.searchuiservice.cts;
import static android.searchuiservice.cts.SearchUiUtils.QUERY_INPUT;
import static android.searchuiservice.cts.SearchUiUtils.QUERY_TIMESTAMP;
import static android.searchuiservice.cts.SearchUiUtils.RESULT_CORPUS1;
import static android.searchuiservice.cts.SearchUiUtils.RESULT_CORPUS2;
import static android.searchuiservice.cts.SearchUiUtils.RESULT_CORPUS3;
import static android.searchuiservice.cts.SearchUiUtils.generateQuery;
import static android.searchuiservice.cts.SearchUiUtils.generateSearchTargetList;
import static androidx.test.InstrumentationRegistry.getContext;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import android.app.search.Query;
import android.app.search.SearchContext;
import android.app.search.SearchSession;
import android.app.search.SearchTarget;
import android.app.search.SearchTargetEvent;
import android.app.search.SearchUiManager;
import android.content.Context;
import android.os.Bundle;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.RequiredServiceRule;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* Tests for {@link SearchUiManager}
*
* atest CtsSearchUiServiceTestCases
*/
@RunWith(AndroidJUnit4.class)
public class SearchUiManagerTest {
private static final String TAG = "SearchUiManagerTest";
private static final boolean DEBUG = false;
private static final long VERIFY_TIMEOUT_MS = 5_000;
private static final long SERVICE_LIFECYCLE_TIMEOUT_MS = 20_000;
private static final int RESULT_TYPES = RESULT_CORPUS1 | RESULT_CORPUS2 | RESULT_CORPUS3;
private static final int TIMEOUT_MS = 150;
private static final Bundle EXTRAS = new Bundle();
@Rule
public final RequiredServiceRule mRequiredServiceRule =
new RequiredServiceRule(Context.SEARCH_UI_SERVICE);
private SearchUiManager mManager;
private SearchSession mClient;
private CtsSearchUiService.Watcher mWatcher;
@Before
public void setUp() throws Exception {
mWatcher = CtsSearchUiService.setWatcher();
mManager = getContext().getSystemService(SearchUiManager.class);
setService(CtsSearchUiService.SERVICE_NAME);
SearchContext searchContext = new SearchContext(RESULT_TYPES, TIMEOUT_MS, EXTRAS);
mClient = mManager.createSearchSession(searchContext);
await(mWatcher.created, "Waiting for onCreate()");
reset(mWatcher.verifier);
}
@After
public void tearDown() throws Exception {
Log.d(TAG, "Starting tear down, watcher is: " + mWatcher);
mClient.close();
setService(null);
await(mWatcher.destroyed, "Waiting for onDestroy()");
mWatcher = null;
CtsSearchUiService.clearWatcher();
}
@Test
public void testCreateSearchSession() {
assertNotNull(mClient);
assertNotNull(mWatcher.verifier);
assertEquals(mWatcher.searchContext.getPackageName(), getContext().getPackageName());
assertEquals(mWatcher.searchContext.getResultTypes(), RESULT_TYPES);
assertEquals(mWatcher.searchContext.getTimeoutMillis(), TIMEOUT_MS);
assertTrue(equalBundles(mWatcher.searchContext.getExtras(), EXTRAS));
}
@Test
public void testSearchContextCtor() {
SearchContext searchContext = new SearchContext(RESULT_TYPES, TIMEOUT_MS);
assertTrue(equalBundles(EXTRAS, searchContext.getExtras()));
}
@Test
public void testQueryCtor() {
Query query = new Query(QUERY_INPUT, QUERY_TIMESTAMP);
assertTrue(equalBundles(EXTRAS, query.getExtras()));
}
public boolean equalBundles(Bundle one, Bundle two) {
if (one.size() != two.size()) {
return false;
}
Set<String> setOne = new HashSet<>(one.keySet());
setOne.addAll(two.keySet());
Object valueOne;
Object valueTwo;
for (String key : setOne) {
if (!one.containsKey(key) || !two.containsKey(key)) {
return false;
}
valueOne = one.get(key);
valueTwo = two.get(key);
if (valueOne instanceof Bundle && valueTwo instanceof Bundle
&& !equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
return false;
} else if (valueOne == null) {
if (valueTwo != null) {
return false;
}
} else if (!valueOne.equals(valueTwo)) {
return false;
}
}
return true;
}
@Test
public void testNotifyEvent() {
String targetId = "sample target id";
int action = SearchTargetEvent.ACTION_SURFACE_VISIBLE;
SearchTargetEvent event = new SearchTargetEvent.Builder(targetId, action)
.setFlags(SearchTargetEvent.FLAG_IME_SHOWN)
.setLaunchLocation("1,0")
.build();
mClient.notifyEvent(generateQuery(new Bundle()), event);
ArgumentCaptor<Query> queryArg = ArgumentCaptor.forClass(Query.class);
ArgumentCaptor<SearchTargetEvent> eventArg
= ArgumentCaptor.forClass(SearchTargetEvent.class);
verify(mWatcher.verifier, timeout(VERIFY_TIMEOUT_MS))
.onNotifyEvent(any(), queryArg.capture(), eventArg.capture());
assertTrue(queryArg.getValue().getInput().equals(QUERY_INPUT));
assertEquals(queryArg.getValue().getTimestampMillis(), QUERY_TIMESTAMP);
assertTrue(eventArg.getValue().getTargetId().equals(targetId));
assertEquals(eventArg.getValue().getAction(), action);
}
@Test
public void testQuery_realCallback() {
Query query = SearchUiUtils.generateQuery(new Bundle());
List<SearchTarget> targets = SearchUiUtils.generateSearchTargetList(3);
final ConsumerVerifier callbackVerifier = new ConsumerVerifier(targets /* expected */);
mWatcher.setTargets(targets /* actual */);
mClient.query(query, Executors.newSingleThreadExecutor(), callbackVerifier);
}
// flaky: 8 failure out of 100
@Ignore
public void testQuery_mockCallback() {
List<SearchTarget> targets = SearchUiUtils.generateSearchTargetList(2);
Query query = SearchUiUtils.generateQuery(new Bundle());
final ConsumerVerifier callbackVerifier = new ConsumerVerifier(targets);
mClient.query(query, Executors.newSingleThreadExecutor(), callbackVerifier);
doAnswer(answer -> {
Consumer<List<SearchTarget>> consumer
= (Consumer<List<SearchTarget>>) answer.getArgument(2);
consumer.accept(targets);
return null;
}).when(mWatcher.verifier).onQuery(any(), any(), any(Consumer.class));
}
@Test
public void testRegisterPredictionUpdatesLifeCycle_realCallback() {
List<SearchTarget> targets = SearchUiUtils.generateSearchTargetList(3);
mWatcher.setTargets(targets /* actual */);
final EmptyQueryResultCallbackVerifier callbackVerifier =
new EmptyQueryResultCallbackVerifier(targets /* expected */);
mClient.registerEmptyQueryResultUpdateCallback(Executors.newSingleThreadExecutor(),
callbackVerifier);
await(mWatcher.startedUpdateEmptyQueryResult,
"Waiting for startedUpdateEmptyQueryResult");
mClient.unregisterEmptyQueryResultUpdateCallback(callbackVerifier);
await(mWatcher.stoppedUpdateEmptyQueryResult,
"Waiting for stoppedUpdateEmptyQueryResult");
}
@Test
public void testQuery_params() {
List<SearchTarget> targets = generateSearchTargetList(2, true, false, false, false);
Bundle extras = new Bundle();
Query query = SearchUiUtils.generateQuery(extras);
final ConsumerVerifier callbackVerifier = new ConsumerVerifier(targets);
mClient.query(query, Executors.newSingleThreadExecutor(), callbackVerifier);
ArgumentCaptor<Query> queryArg = ArgumentCaptor.forClass(Query.class);
ArgumentCaptor<Consumer<List<SearchTarget>>> callbackArg
= ArgumentCaptor.forClass(Consumer.class);
verify(mWatcher.verifier, timeout(VERIFY_TIMEOUT_MS))
.onQuery(any(), queryArg.capture(), callbackArg.capture());
Query expectedQuery = queryArg.getValue();
assertTrue(expectedQuery.getInput().equals(QUERY_INPUT));
assertEquals(expectedQuery.getTimestampMillis(), QUERY_TIMESTAMP);
assertTrue(equalBundles(expectedQuery.getExtras(), extras));
}
private void setService(String service) {
Log.d(TAG, "Setting search ui service to " + service);
int userId = Process.myUserHandle().getIdentifier();
if (service != null) {
runShellCommand("cmd search_ui set temporary-service "
+ userId + " " + service + " 60000");
} else {
runShellCommand("cmd search_ui set temporary-service " + userId);
}
}
private void await(@NonNull CountDownLatch latch, @NonNull String message) {
try {
assertWithMessage(message).that(
latch.await(SERVICE_LIFECYCLE_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while: " + message);
}
}
private void runShellCommand(String command) {
Log.d(TAG, "runShellCommand(): " + command);
try {
SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
} catch (Exception e) {
throw new RuntimeException("Command '" + command + "' failed: ", e);
}
}
public static class ConsumerVerifier implements
Consumer<List<SearchTarget>> {
private static List<SearchTarget> mExpectedTargets;
public ConsumerVerifier(List<SearchTarget> targets) {
mExpectedTargets = targets;
}
@Override
public void accept(List<SearchTarget> actualTargets) {
if (DEBUG) {
Log.d(TAG, "ConsumerVerifier.accept targets.size= " + actualTargets.size());
Log.d(TAG, "ConsumerVerifier.accept target(1).packageName=" + actualTargets.get(
0).getPackageName());
}
Assert.assertArrayEquals(actualTargets.toArray(), mExpectedTargets.toArray());
}
}
public static class EmptyQueryResultCallbackVerifier implements
SearchSession.Callback {
private static List<SearchTarget> sExpectedTargets;
public EmptyQueryResultCallbackVerifier(List<SearchTarget> targets) {
sExpectedTargets = targets;
}
@Override
public void onTargetsAvailable(List<SearchTarget> actualTargets) {
if (DEBUG) {
Log.d(TAG, "ZeroStateCallbackVerifier.onTargetsAvailable targets.size= "
+ actualTargets.size());
Log.d(TAG, "ZeroStateCallbackVerifier.onTargetsAvailable target(1).packageName="
+ actualTargets.get(0).getPackageName());
}
Assert.assertArrayEquals(actualTargets.toArray(), sExpectedTargets.toArray());
}
}
}