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