blob: 338c3618964e0c2075ddef26dc021578c85626f7 [file] [log] [blame]
/*
* Copyright (C) 2019 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 andf
* limitations under the License.
*/
package android.voiceinteraction.cts;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.app.DirectAction;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteCallback;
import android.platform.test.annotations.AppModeFull;
import android.util.Log;
import android.voiceinteraction.common.Utils;
import com.android.compatibility.common.util.ThrowingRunnable;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Tests for the direction action related functions.
*/
public class DirectActionsTest extends AbstractVoiceInteractionTestCase {
private static final String TAG = DirectActionsTest.class.getSimpleName();
private static final long OPERATION_TIMEOUT_MS = 5000;
private final @NonNull SessionControl mSessionControl = new SessionControl();
private final @NonNull ActivityControl mActivityControl = new ActivityControl();
@Test
public void testPerformDirectAction() throws Exception {
mActivityControl.startActivity();
mSessionControl.startVoiceInteractionSession();
try {
// Get the actions.
final List<DirectAction> actions = mSessionControl.getDirectActions();
Log.v(TAG, "actions: " + actions);
// Only the expected action should be reported
final DirectAction action = getExpectedDirectActionAssertively(actions);
// Perform the expected action.
final Bundle result = mSessionControl.performDirectAction(action,
createActionArguments());
// Assert the action completed successfully.
assertActionSucceeded(result);
} finally {
mSessionControl.stopVoiceInteractionSession();
mActivityControl.finishActivity();
}
}
@AppModeFull(reason = "testPerformDirectAction() is enough")
@Test
public void testCancelPerformedDirectAction() throws Exception {
mActivityControl.startActivity();
mSessionControl.startVoiceInteractionSession();
try {
// Get the actions.
final List<DirectAction> actions = mSessionControl.getDirectActions();
Log.v(TAG, "actions: " + actions);
// Only the expected action should be reported
final DirectAction action = getExpectedDirectActionAssertively(actions);
// Perform the expected action.
final Bundle result = mSessionControl.performDirectActionAndCancel(action,
createActionArguments());
// Assert the action was cancelled.
assertActionCancelled(result);
} finally {
mSessionControl.stopVoiceInteractionSession();
mActivityControl.finishActivity();
}
}
@AppModeFull(reason = "testPerformDirectAction() is enough")
@Test
public void testVoiceInteractorDestroy() throws Exception {
mActivityControl.startActivity();
mSessionControl.startVoiceInteractionSession();
try {
// Get the actions to set up the VoiceInteractor
mSessionControl.getDirectActions();
assertThat(mActivityControl
.detectInteractorDestroyed(() -> mSessionControl.stopVoiceInteractionSession()))
.isTrue();
} finally {
mSessionControl.stopVoiceInteractionSession();
mActivityControl.finishActivity();
}
}
@AppModeFull(reason = "testPerformDirectAction() is enough")
@Test
public void testNotifyDirectActionsChanged() throws Exception {
mActivityControl.startActivity();
mSessionControl.startVoiceInteractionSession();
try {
// Get the actions to set up the VoiceInteractor
mSessionControl.getDirectActions();
assertThat(mSessionControl.detectDirectActionsInvalidated(
() -> mActivityControl.invalidateDirectActions())).isTrue();
} finally {
mSessionControl.stopVoiceInteractionSession();
mActivityControl.finishActivity();
}
}
private final class SessionControl {
private @Nullable RemoteCallback mControl;
private void startVoiceInteractionSession() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final RemoteCallback callback = new RemoteCallback((result) -> {
mControl = result.getParcelable(Utils.DIRECT_ACTIONS_KEY_CONTROL);
latch.countDown();
});
final Intent intent = new Intent();
intent.putExtra(Utils.DIRECT_ACTIONS_KEY_CLASS,
"android.voiceinteraction.service.DirectActionsSession");
intent.setClassName("android.voiceinteraction.service",
"android.voiceinteraction.service.VoiceInteractionMain");
intent.putExtra(Utils.DIRECT_ACTIONS_KEY_CALLBACK, callback);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.v(TAG, "startVoiceInteractionSession(): " + intent);
mContext.startActivity(intent);
if (!latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("actitvity not started in " + OPERATION_TIMEOUT_MS
+ "ms");
}
}
private void stopVoiceInteractionSession() throws Exception {
executeCommand(Utils.DIRECT_ACTIONS_SESSION_CMD_FINISH,
null /*directAction*/, null /*arguments*/, null /*postActionCommand*/);
}
@Nullable List<DirectAction> getDirectActions() throws Exception {
final ArrayList<DirectAction> actions = new ArrayList<>();
final Bundle result = executeCommand(Utils.DIRECT_ACTIONS_SESSION_CMD_GET_ACTIONS,
null /*directAction*/, null /*arguments*/, null /*postActionCommand*/);
actions.addAll(result.getParcelableArrayList(Utils.DIRECT_ACTIONS_KEY_RESULT));
return actions;
}
@Nullable Bundle performDirectAction(@NonNull DirectAction directAction,
@NonNull Bundle arguments) throws Exception {
return executeCommand(Utils.DIRECT_ACTIONS_SESSION_CMD_PERFORM_ACTION,
directAction, arguments, null /*postActionCommand*/);
}
@Nullable Bundle performDirectActionAndCancel(@NonNull DirectAction directAction,
@NonNull Bundle arguments) throws Exception {
return executeCommand(Utils.DIRECT_ACTIONS_SESSION_CMD_PERFORM_ACTION_CANCEL,
directAction, arguments, null /*postActionCommand*/);
}
@Nullable
boolean detectDirectActionsInvalidated(@NonNull ThrowingRunnable postActionCommand)
throws Exception {
final Bundle result = executeCommand(
Utils.DIRECT_ACTIONS_SESSION_CMD_DETECT_ACTIONS_CHANGED,
null /*directAction*/, null /*arguments*/, postActionCommand);
return result.getBoolean(Utils.DIRECT_ACTIONS_KEY_RESULT);
}
@Nullable Bundle executeCommand(@NonNull String action, @Nullable DirectAction directAction,
@Nullable Bundle arguments, @Nullable ThrowingRunnable postActionCommand)
throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final Bundle result = new Bundle();
final RemoteCallback callback = new RemoteCallback((b) -> {
result.putAll(b);
latch.countDown();
});
final Bundle command = new Bundle();
command.putString(Utils.DIRECT_ACTIONS_KEY_COMMAND, action);
command.putParcelable(Utils.DIRECT_ACTIONS_KEY_ACTION, directAction);
command.putBundle(Utils.DIRECT_ACTIONS_KEY_ARGUMENTS, arguments);
command.putParcelable(Utils.DIRECT_ACTIONS_KEY_CALLBACK, callback);
Log.v(TAG, "executeCommand(): action=" + action + " command="
+ Utils.toBundleString(command));
mControl.sendResult(command);
if (postActionCommand != null) {
Log.v(TAG, "Executing post-action command for " + action);
postActionCommand.run();
}
if (!latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("result not received in " + OPERATION_TIMEOUT_MS + "ms");
}
Log.v(TAG, "returning " + Utils.toBundleString(result));
return result;
}
}
private final class ActivityControl {
private @Nullable RemoteCallback mControl;
void startActivity() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final RemoteCallback callback = new RemoteCallback((result) -> {
Log.v(TAG, "ActivityControl: testapp called the callback: "
+ Utils.toBundleString(result));
mControl = result.getParcelable(Utils.DIRECT_ACTIONS_KEY_CONTROL);
latch.countDown();
});
final Intent intent = new Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(Uri.parse("https://android.voiceinteraction.testapp"
+ "/DirectActionsActivity"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Utils.DIRECT_ACTIONS_KEY_CALLBACK, callback);
if (!mContext.getPackageManager().isInstantApp()) {
intent.setPackage("android.voiceinteraction.testapp");
}
Log.v(TAG, "startActivity: " + intent);
mContext.startActivity(intent);
if (!latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("actitvity not started in " + OPERATION_TIMEOUT_MS
+ "ms");
}
}
private boolean detectInteractorDestroyed(ThrowingRunnable destroyTrigger)
throws Exception {
final Bundle result = executeRemoteCommand(
Utils.DIRECT_ACTIONS_ACTIVITY_CMD_DESTROYED_INTERACTOR,
destroyTrigger);
return result.getBoolean(Utils.DIRECT_ACTIONS_KEY_RESULT);
}
void finishActivity() throws Exception {
executeRemoteCommand(Utils.DIRECT_ACTIONS_ACTIVITY_CMD_FINISH);
}
void invalidateDirectActions() throws Exception {
executeRemoteCommand(Utils.DIRECT_ACTIONS_ACTIVITY_CMD_INVALIDATE_ACTIONS);
}
@NonNull Bundle executeRemoteCommand(@NonNull String action) throws Exception {
return executeRemoteCommand(action, /* postActionCommand= */ null);
}
@NonNull Bundle executeRemoteCommand(@NonNull String action,
@Nullable ThrowingRunnable postActionCommand) throws Exception {
final Bundle result = new Bundle();
final CountDownLatch latch = new CountDownLatch(1);
final RemoteCallback callback = new RemoteCallback((b) -> {
Log.v(TAG, "executeRemoteCommand(): received result from '" + action + "': "
+ Utils.toBundleString(b));
if (b != null) {
result.putAll(b);
}
latch.countDown();
});
final Bundle command = new Bundle();
command.putString(Utils.DIRECT_ACTIONS_KEY_COMMAND, action);
command.putParcelable(Utils.DIRECT_ACTIONS_KEY_CALLBACK, callback);
Log.v(TAG, "executeRemoteCommand(): sending command for '" + action + "'");
mControl.sendResult(command);
if (postActionCommand != null) {
try {
postActionCommand.run();
} catch (TimeoutException e) {
Log.e(TAG, "action '" + action + "' timed out" );
} catch (Exception e) {
throw e;
}
}
if (!latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("result not received in " + OPERATION_TIMEOUT_MS + "ms");
}
return result;
}
}
private @NonNull DirectAction getExpectedDirectActionAssertively(
@Nullable List<DirectAction> actions) {
assertWithMessage("no actions").that(actions).isNotEmpty();
final DirectAction action = actions.get(0);
assertThat(action.getId()).isEqualTo(Utils.DIRECT_ACTIONS_ACTION_ID);
assertThat(action.getExtras().getString(Utils.DIRECT_ACTION_EXTRA_KEY))
.isEqualTo(Utils.DIRECT_ACTION_EXTRA_VALUE);
assertThat(action.getLocusId().getId()).isEqualTo(Utils.DIRECT_ACTIONS_LOCUS_ID.getId());
return action;
}
private @NonNull Bundle createActionArguments() {
final Bundle args = new Bundle();
args.putString(Utils.DIRECT_ACTIONS_KEY_ARGUMENTS, Utils.DIRECT_ACTIONS_KEY_ARGUMENTS);
Log.v(TAG, "createActionArguments(): " + Utils.toBundleString(args));
return args;
}
private void assertActionSucceeded(@NonNull Bundle result) {
final Bundle bundle = result.getBundle(Utils.DIRECT_ACTIONS_KEY_RESULT);
final String status = bundle.getString(Utils.DIRECT_ACTIONS_KEY_RESULT);
assertWithMessage("assertActionSucceeded(%s)", Utils.toBundleString(result))
.that(Utils.DIRECT_ACTIONS_RESULT_PERFORMED).isEqualTo(status);
}
private void assertActionCancelled(@NonNull Bundle result) {
final Bundle bundle = result.getBundle(Utils.DIRECT_ACTIONS_KEY_RESULT);
final String status = bundle.getString(Utils.DIRECT_ACTIONS_KEY_RESULT);
assertWithMessage("assertActionCancelled(%s)", Utils.toBundleString(result))
.that(Utils.DIRECT_ACTIONS_RESULT_CANCELLED).isEqualTo(status);
}
}