1 /* 2 * Copyright (C) 2019 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.voiceinteraction.cts; 18 19 import static com.android.compatibility.common.util.ShellUtils.runShellCommand; 20 21 import static com.google.common.truth.Truth.assertThat; 22 import static com.google.common.truth.Truth.assertWithMessage; 23 24 import android.app.DirectAction; 25 import android.content.Intent; 26 import android.content.pm.PackageInfo; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.RemoteCallback; 30 import android.platform.test.annotations.AppModeFull; 31 import android.util.Log; 32 import android.voiceinteraction.common.Utils; 33 import android.voiceinteraction.cts.testcore.VoiceInteractionSessionControl; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 38 import com.android.compatibility.common.util.ThrowingRunnable; 39 40 import org.junit.Test; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.concurrent.CountDownLatch; 45 import java.util.concurrent.TimeUnit; 46 import java.util.concurrent.TimeoutException; 47 48 /** 49 * Tests for the direction action related functions. 50 */ 51 public class DirectActionsTest extends AbstractVoiceInteractionTestCase { 52 private static final String TAG = DirectActionsTest.class.getSimpleName(); 53 54 private final @NonNull SessionControl mSessionControl = new SessionControl(); 55 private final @NonNull ActivityControl mActivityControl = new ActivityControl(); 56 57 @Test testPerformDirectAction()58 public void testPerformDirectAction() throws Exception { 59 mActivityControl.startActivity(); 60 mSessionControl.startVoiceInteractionSession(); 61 try { 62 // Get the actions. 63 final List<DirectAction> actions = mSessionControl.getDirectActions(); 64 Log.v(TAG, "actions: " + actions); 65 66 // Only the expected action should be reported 67 final DirectAction action = getExpectedDirectActionAssertively(actions); 68 69 // Perform the expected action. 70 final Bundle result = mSessionControl.performDirectAction(action, 71 createActionArguments()); 72 73 // Assert the action completed successfully. 74 assertActionSucceeded(result); 75 } finally { 76 mSessionControl.stopVoiceInteractionSession(); 77 mActivityControl.finishActivity(); 78 } 79 } 80 81 @AppModeFull(reason = "testPerformDirectAction() is enough") 82 @Test testGetPackageName()83 public void testGetPackageName() throws Exception { 84 mActivityControl.startActivity(); 85 mSessionControl.startVoiceInteractionSession(); 86 try { 87 // Get the actions to set up the VoiceInteractor 88 mSessionControl.getDirectActions(); 89 90 String packageName = mActivityControl.getPackageName(); 91 assertThat(packageName).isEqualTo("android.voiceinteraction.service"); 92 } finally { 93 mSessionControl.stopVoiceInteractionSession(); 94 mActivityControl.finishActivity(); 95 } 96 } 97 98 @AppModeFull(reason = "testPerformDirectAction() is enough") 99 @Test testGrantVisibilityOnRequestDirectActions()100 public void testGrantVisibilityOnRequestDirectActions() throws Exception { 101 mActivityControl.startActivity(); 102 mSessionControl.startVoiceInteractionSession(); 103 try { 104 // Get the actions to set up the VoiceInteractor, which triggers implicit visibility 105 // granting. 106 mSessionControl.getDirectActions(); 107 108 // If visibility is granted, then package info is retrieved successfully. 109 PackageInfo packageInfo = mActivityControl.getPackageInfo(); 110 assertThat(packageInfo).isNotNull(); 111 } finally { 112 mSessionControl.stopVoiceInteractionSession(); 113 mActivityControl.finishActivity(); 114 } 115 } 116 117 @AppModeFull(reason = "testPerformDirectAction() is enough") 118 @Test testCancelPerformedDirectAction()119 public void testCancelPerformedDirectAction() throws Exception { 120 mActivityControl.startActivity(); 121 mSessionControl.startVoiceInteractionSession(); 122 try { 123 // Get the actions. 124 final List<DirectAction> actions = mSessionControl.getDirectActions(); 125 Log.v(TAG, "actions: " + actions); 126 127 // Only the expected action should be reported 128 final DirectAction action = getExpectedDirectActionAssertively(actions); 129 130 // Perform the expected action. 131 final Bundle result = mSessionControl.performDirectActionAndCancel(action, 132 createActionArguments()); 133 134 // Assert the action was cancelled. 135 assertActionCancelled(result); 136 } finally { 137 mSessionControl.stopVoiceInteractionSession(); 138 mActivityControl.finishActivity(); 139 } 140 } 141 142 @AppModeFull(reason = "testPerformDirectAction() is enough") 143 @Test testVoiceInteractorDestroy()144 public void testVoiceInteractorDestroy() throws Exception { 145 mActivityControl.startActivity(); 146 mSessionControl.startVoiceInteractionSession(); 147 try { 148 // Get the actions to set up the VoiceInteractor 149 mSessionControl.getDirectActions(); 150 151 assertThat(mActivityControl 152 .detectInteractorDestroyed(() -> mSessionControl.stopVoiceInteractionSession())) 153 .isTrue(); 154 } finally { 155 mSessionControl.stopVoiceInteractionSession(); 156 mActivityControl.finishActivity(); 157 } 158 } 159 160 @AppModeFull(reason = "testPerformDirectAction() is enough") 161 @Test testNotifyDirectActionsChanged()162 public void testNotifyDirectActionsChanged() throws Exception { 163 mActivityControl.startActivity(); 164 mSessionControl.startVoiceInteractionSession(); 165 try { 166 // Get the actions to set up the VoiceInteractor 167 mSessionControl.getDirectActions(); 168 169 assertThat(mSessionControl.detectDirectActionsInvalidated( 170 () -> mActivityControl.invalidateDirectActions())).isTrue(); 171 } finally { 172 mSessionControl.stopVoiceInteractionSession(); 173 mActivityControl.finishActivity(); 174 } 175 } 176 177 private final class SessionControl extends VoiceInteractionSessionControl { 178 SessionControl()179 SessionControl() { 180 super(mContext); 181 } 182 startVoiceInteractionSession()183 private void startVoiceInteractionSession() throws Exception { 184 final Intent intent = new Intent(); 185 intent.putExtra(Utils.VOICE_INTERACTION_KEY_CLASS, 186 "android.voiceinteraction.service.DirectActionsSession"); 187 intent.setClassName("android.voiceinteraction.service", 188 "android.voiceinteraction.service.VoiceInteractionMain"); 189 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 190 191 startVoiceInteractionSession(intent); 192 } 193 getDirectActions()194 @Nullable List<DirectAction> getDirectActions() throws Exception { 195 final ArrayList<DirectAction> actions = new ArrayList<>(); 196 final Bundle result = executeCommand(Utils.DIRECT_ACTIONS_SESSION_CMD_GET_ACTIONS, 197 null /*directAction*/, null /*arguments*/, null /*postActionCommand*/); 198 actions.addAll(result.getParcelableArrayList(Utils.DIRECT_ACTIONS_KEY_RESULT)); 199 return actions; 200 } 201 performDirectAction(@onNull DirectAction directAction, @NonNull Bundle arguments)202 @Nullable Bundle performDirectAction(@NonNull DirectAction directAction, 203 @NonNull Bundle arguments) throws Exception { 204 return executeCommand(Utils.DIRECT_ACTIONS_SESSION_CMD_PERFORM_ACTION, 205 directAction, arguments, null /*postActionCommand*/); 206 } 207 performDirectActionAndCancel(@onNull DirectAction directAction, @NonNull Bundle arguments)208 @Nullable Bundle performDirectActionAndCancel(@NonNull DirectAction directAction, 209 @NonNull Bundle arguments) throws Exception { 210 return executeCommand(Utils.DIRECT_ACTIONS_SESSION_CMD_PERFORM_ACTION_CANCEL, 211 directAction, arguments, null /*postActionCommand*/); 212 } 213 214 @Nullable detectDirectActionsInvalidated(@onNull ThrowingRunnable postActionCommand)215 boolean detectDirectActionsInvalidated(@NonNull ThrowingRunnable postActionCommand) 216 throws Exception { 217 final Bundle result = executeCommand( 218 Utils.DIRECT_ACTIONS_SESSION_CMD_DETECT_ACTIONS_CHANGED, 219 null /*directAction*/, null /*arguments*/, postActionCommand); 220 return result.getBoolean(Utils.DIRECT_ACTIONS_KEY_RESULT); 221 } 222 } 223 224 private final class ActivityControl { 225 private @Nullable RemoteCallback mControl; 226 startActivity()227 void startActivity() throws Exception { 228 final CountDownLatch latch = new CountDownLatch(1); 229 230 final RemoteCallback callback = new RemoteCallback((result) -> { 231 Log.v(TAG, "ActivityControl: testapp called the callback: " 232 + Utils.toBundleString(result)); 233 mControl = result.getParcelable(Utils.VOICE_INTERACTION_KEY_CONTROL); 234 latch.countDown(); 235 }); 236 237 final Intent intent = new Intent() 238 .setAction(Intent.ACTION_VIEW) 239 .addCategory(Intent.CATEGORY_BROWSABLE) 240 .setData(Uri.parse("https://android.voiceinteraction.testapp" 241 + "/DirectActionsActivity")) 242 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 243 .putExtra(Utils.VOICE_INTERACTION_KEY_CALLBACK, callback); 244 if (mContext.getPackageManager().isInstantApp()) { 245 // Override app-links domain verification. 246 runShellCommand( 247 String.format( 248 "pm set-app-links-user-selection --user cur --package %1$s true" 249 + " %1$s", 250 Utils.TEST_APP_PACKAGE)); 251 } else { 252 intent.setPackage(Utils.TEST_APP_PACKAGE); 253 } 254 255 Log.v(TAG, "startActivity: " + intent); 256 mContext.startActivity(intent); 257 258 final long timeoutMs = Utils.getAdjustedOperationTimeoutMs(); 259 if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) { 260 throw new TimeoutException( 261 "activity not started in " + timeoutMs + "ms"); 262 } 263 } 264 detectInteractorDestroyed(ThrowingRunnable destroyTrigger)265 private boolean detectInteractorDestroyed(ThrowingRunnable destroyTrigger) 266 throws Exception { 267 final Bundle result = executeRemoteCommand( 268 Utils.DIRECT_ACTIONS_ACTIVITY_CMD_DESTROYED_INTERACTOR, 269 destroyTrigger); 270 return result.getBoolean(Utils.DIRECT_ACTIONS_KEY_RESULT); 271 } 272 getPackageName()273 private String getPackageName() 274 throws Exception { 275 final Bundle result = executeRemoteCommand( 276 Utils.DIRECT_ACTIONS_ACTIVITY_CMD_GET_PACKAGE_NAME); 277 return result.getString(Utils.DIRECT_ACTIONS_KEY_RESULT); 278 } 279 getPackageInfo()280 private PackageInfo getPackageInfo() throws Exception { 281 final Bundle result = executeRemoteCommand( 282 Utils.DIRECT_ACTIONS_ACTIVITY_CMD_GET_PACKAGE_INFO); 283 return (PackageInfo) result.getParcelable(Utils.DIRECT_ACTIONS_KEY_RESULT); 284 } 285 finishActivity()286 void finishActivity() throws Exception { 287 executeRemoteCommand(Utils.VOICE_INTERACTION_ACTIVITY_CMD_FINISH); 288 } 289 invalidateDirectActions()290 void invalidateDirectActions() throws Exception { 291 executeRemoteCommand(Utils.DIRECT_ACTIONS_ACTIVITY_CMD_INVALIDATE_ACTIONS); 292 } 293 executeRemoteCommand(@onNull String action)294 @NonNull Bundle executeRemoteCommand(@NonNull String action) throws Exception { 295 return executeRemoteCommand(action, /* postActionCommand= */ null); 296 } 297 executeRemoteCommand(@onNull String action, @Nullable ThrowingRunnable postActionCommand)298 @NonNull Bundle executeRemoteCommand(@NonNull String action, 299 @Nullable ThrowingRunnable postActionCommand) throws Exception { 300 final Bundle result = new Bundle(); 301 302 final CountDownLatch latch = new CountDownLatch(1); 303 304 final RemoteCallback callback = new RemoteCallback((b) -> { 305 Log.v(TAG, "executeRemoteCommand(): received result from '" + action + "': " 306 + Utils.toBundleString(b)); 307 if (b != null) { 308 result.putAll(b); 309 } 310 latch.countDown(); 311 }); 312 313 final Bundle command = new Bundle(); 314 command.putString(Utils.VOICE_INTERACTION_KEY_COMMAND, action); 315 command.putParcelable(Utils.VOICE_INTERACTION_KEY_CALLBACK, callback); 316 317 Log.v(TAG, "executeRemoteCommand(): sending command for '" + action + "'"); 318 mControl.sendResult(command); 319 320 if (postActionCommand != null) { 321 try { 322 postActionCommand.run(); 323 } catch (Exception e) { 324 Log.e(TAG, "action '" + action + "' failed"); 325 throw e; 326 } 327 } 328 329 final long timeoutMs = Utils.getAdjustedOperationTimeoutMs(); 330 if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) { 331 throw new TimeoutException( 332 "result not received in " + timeoutMs + "ms"); 333 } 334 return result; 335 } 336 } 337 getExpectedDirectActionAssertively( @ullable List<DirectAction> actions)338 private @NonNull DirectAction getExpectedDirectActionAssertively( 339 @Nullable List<DirectAction> actions) { 340 assertWithMessage("no actions").that(actions).isNotEmpty(); 341 final DirectAction action = actions.get(0); 342 assertThat(action.getId()).isEqualTo(Utils.DIRECT_ACTIONS_ACTION_ID); 343 assertThat(action.getExtras().getString(Utils.DIRECT_ACTION_EXTRA_KEY)) 344 .isEqualTo(Utils.DIRECT_ACTION_EXTRA_VALUE); 345 assertThat(action.getLocusId().getId()).isEqualTo(Utils.DIRECT_ACTIONS_LOCUS_ID.getId()); 346 return action; 347 } 348 createActionArguments()349 private @NonNull Bundle createActionArguments() { 350 final Bundle args = new Bundle(); 351 args.putString(Utils.VOICE_INTERACTION_KEY_ARGUMENTS, 352 Utils.VOICE_INTERACTION_KEY_ARGUMENTS); 353 Log.v(TAG, "createActionArguments(): " + Utils.toBundleString(args)); 354 return args; 355 } 356 assertActionSucceeded(@onNull Bundle result)357 private void assertActionSucceeded(@NonNull Bundle result) { 358 final Bundle bundle = result.getBundle(Utils.DIRECT_ACTIONS_KEY_RESULT); 359 final String status = bundle.getString(Utils.DIRECT_ACTIONS_KEY_RESULT); 360 assertWithMessage("assertActionSucceeded(%s)", Utils.toBundleString(result)) 361 .that(Utils.DIRECT_ACTIONS_RESULT_PERFORMED).isEqualTo(status); 362 } 363 assertActionCancelled(@onNull Bundle result)364 private void assertActionCancelled(@NonNull Bundle result) { 365 final Bundle bundle = result.getBundle(Utils.DIRECT_ACTIONS_KEY_RESULT); 366 final String status = bundle.getString(Utils.DIRECT_ACTIONS_KEY_RESULT); 367 assertWithMessage("assertActionCancelled(%s)", Utils.toBundleString(result)) 368 .that(Utils.DIRECT_ACTIONS_RESULT_CANCELLED).isEqualTo(status); 369 } 370 } 371 372