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