1 /*
2  * Copyright (C) 2024 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 com.android.compatibility.common.util.concurrentuser;
18 
19 import static android.Manifest.permission.CREATE_USERS;
20 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
21 import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
22 
23 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
24 
25 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 
29 import static org.junit.Assert.assertNotEquals;
30 import static org.junit.Assert.fail;
31 
32 import static java.util.concurrent.TimeUnit.MILLISECONDS;
33 
34 import android.content.ComponentName;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.content.pm.UserInfo;
38 import android.os.Bundle;
39 import android.os.RemoteCallback;
40 import android.os.UserHandle;
41 import android.os.UserManager;
42 
43 import androidx.annotation.Nullable;
44 
45 import java.util.List;
46 import java.util.concurrent.CountDownLatch;
47 
48 /** Provides utility methods to interact with {@link ConcurrentUserActivityBase}. */
49 public final class ConcurrentUserActivityUtils {
ConcurrentUserActivityUtils()50     private ConcurrentUserActivityUtils() {
51     }
52 
53     static final String BROADCAST_ACTION_TRIGGER = "broadcast_action_trigger";
54     static final String KEY_USER_ID = "key_user_id";
55     static final String KEY_CALLBACK = "key_callback";
56     static final String KEY_BUNDLE = "key_bundle";
57 
58     private static final long LAUNCH_ACTIVITY_TIMEOUT_MS = 3000;
59     private static final long SEND_BROADCAST_TIMEOUT_MS = 3000;
60 
61     /**
62      * Gets the user that responds to the test. See {@link ConcurrentUserActivityBase}.
63      *
64      * @throws AssertionError if such user doesn't exist
65      */
getResponderUserId()66     public static int getResponderUserId() {
67         UserManager userManager =
68                 getInstrumentation().getTargetContext().getSystemService(UserManager.class);
69         int initiatorUserId = getInstrumentation().getTargetContext().getUserId();
70         int[] responderUserId = new int[]{UserHandle.USER_NULL};
71         runWithShellPermissionIdentity(
72                 () -> {
73                     List<UserInfo> users = userManager.getAliveUsers();
74                     for (UserInfo info : users) {
75                         if (info.id != initiatorUserId && info.isFull() && info.isInitialized()) {
76                             responderUserId[0] = info.id;
77                             break;
78                         }
79                     }
80                 }, CREATE_USERS);
81 
82         assertNotEquals("Failed to find the responder user on the device",
83                 UserHandle.USER_NULL, responderUserId[0]);
84         return responderUserId[0];
85     }
86 
87     /**
88      * Launches the given activity as the given {@code userId} and waits for it to be launched.
89      *
90      * @param activityName the ComponentName of the Activity, which must be a subclass of
91      *                     {@link ConcurrentUserActivityBase}
92      * @param userId       the user ID
93      */
launchActivityAsUserSync(ComponentName activityName, int userId)94     public static void launchActivityAsUserSync(ComponentName activityName, int userId) {
95         CountDownLatch latch = new CountDownLatch(1);
96         RemoteCallback callback = new RemoteCallback(bundle -> {
97             int actualUserId = bundle.getInt(KEY_USER_ID);
98             assertThat(actualUserId).isEqualTo(userId);
99             latch.countDown();
100         });
101 
102         Context context = getInstrumentation().getContext();
103         UserHandle userHandle = UserHandle.of(userId);
104         Intent intent = new Intent(Intent.ACTION_MAIN)
105                 .setClassName(activityName.getPackageName(), activityName.getClassName())
106                 .putExtra(KEY_CALLBACK, callback)
107                 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
108         runWithShellPermissionIdentity(
109                 () -> context.startActivityAsUser(intent, userHandle),
110                 // INTERNAL_SYSTEM_WINDOW is needed to launch the activity on secondary display.
111                 INTERACT_ACROSS_USERS_FULL, INTERNAL_SYSTEM_WINDOW);
112         try {
113             if (!latch.await(LAUNCH_ACTIVITY_TIMEOUT_MS, MILLISECONDS)) {
114                 fail(String.format("Failed to launch activity %s as user %d", activityName,
115                         userId));
116             }
117         } catch (InterruptedException e) {
118             Thread.currentThread().interrupt();
119             throw new RuntimeException(e);
120         }
121     }
122 
123     /**
124      * Sends a message to the testing app running as the given user, and waits for its reply.
125      *
126      * @param packageName    the package name of the receiver app, which has a
127      *                       {@link ConcurrentUserActivityBase}
128      * @param receiverUserId the user ID of the receiver app
129      * @param bundleToSend   the Bundle to be sent to the receiver app
130      * @return the Bundle sent from the receiver app
131      */
132     @Nullable
sendBundleAndWaitForReply(String packageName, int receiverUserId, Bundle bundleToSend)133     public static Bundle sendBundleAndWaitForReply(String packageName, int receiverUserId,
134             Bundle bundleToSend) {
135         Bundle[] receivedBundle = new Bundle[1];
136         CountDownLatch latch = new CountDownLatch(1);
137         RemoteCallback callback = new RemoteCallback(bundle -> {
138             receivedBundle[0] = bundle;
139             latch.countDown();
140         });
141 
142         Intent intent = new Intent(BROADCAST_ACTION_TRIGGER)
143                 .setPackage(packageName)
144                 .putExtra(KEY_BUNDLE, bundleToSend)
145                 .putExtra(KEY_CALLBACK, callback);
146         runWithShellPermissionIdentity(
147                 () -> getInstrumentation().getContext().sendBroadcastAsUser(
148                         intent, UserHandle.of(receiverUserId)),
149                 INTERACT_ACROSS_USERS_FULL);
150         try {
151             if (!latch.await(SEND_BROADCAST_TIMEOUT_MS, MILLISECONDS)) {
152                 fail(String.format("Failed to send %s to %s (user %d)", bundleToSend,
153                         packageName, receiverUserId));
154             }
155         } catch (InterruptedException e) {
156             Thread.currentThread().interrupt();
157             throw new RuntimeException(e);
158         }
159         return receivedBundle[0];
160     }
161 }
162