1 /* 2 * Copyright (C) 2022 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.appenumeration.cts; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.appenumeration.cts.Constants.ACTIVITY_CLASS_TEST; 21 import static android.appenumeration.cts.Constants.EXTRA_DATA; 22 import static android.appenumeration.cts.Constants.EXTRA_ERROR; 23 import static android.appenumeration.cts.Constants.EXTRA_ID; 24 import static android.appenumeration.cts.Constants.EXTRA_PENDING_INTENT; 25 import static android.appenumeration.cts.Constants.EXTRA_REMOTE_CALLBACK; 26 import static android.appenumeration.cts.Constants.EXTRA_REMOTE_READY_CALLBACK; 27 import static android.appenumeration.cts.Constants.EXTRA_SEGMENT_RESULT; 28 import static android.appenumeration.cts.Constants.EXTRA_SEGMENT_SIZE; 29 import static android.appenumeration.cts.Utils.Result; 30 import static android.appenumeration.cts.Utils.ThrowingBiFunction; 31 import static android.appenumeration.cts.Utils.ThrowingFunction; 32 import static android.os.Process.INVALID_UID; 33 34 import static org.hamcrest.MatcherAssert.assertThat; 35 import static org.hamcrest.Matchers.hasItemInArray; 36 import static org.hamcrest.Matchers.not; 37 import static org.hamcrest.Matchers.notNullValue; 38 39 import android.app.ActivityOptions; 40 import android.app.PendingIntent; 41 import android.content.ComponentName; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.content.pm.PackageInstaller; 45 import android.content.pm.PackageManager; 46 import android.net.Uri; 47 import android.os.Bundle; 48 import android.os.ConditionVariable; 49 import android.os.Handler; 50 import android.os.HandlerThread; 51 import android.os.Parcelable; 52 import android.os.RemoteCallback; 53 54 import androidx.annotation.NonNull; 55 import androidx.annotation.Nullable; 56 import androidx.test.platform.app.InstrumentationRegistry; 57 58 import com.android.compatibility.common.util.AmUtils; 59 60 import org.junit.AfterClass; 61 import org.junit.BeforeClass; 62 import org.junit.Rule; 63 import org.junit.rules.TestName; 64 65 import java.util.ArrayList; 66 import java.util.List; 67 import java.util.Objects; 68 import java.util.UUID; 69 import java.util.concurrent.TimeUnit; 70 import java.util.concurrent.TimeoutException; 71 import java.util.concurrent.atomic.AtomicReference; 72 73 public class AppEnumerationTestsBase { 74 private static Handler sResponseHandler; 75 private static HandlerThread sResponseThread; 76 77 static Context sContext; 78 static PackageManager sPm; 79 80 static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(15); 81 static final long DEFAULT_TIMEOUT_WAIT_FOR_READY_MS = TimeUnit.SECONDS.toMillis(30); 82 83 84 @Rule 85 public TestName name = new TestName(); 86 87 @BeforeClass setup()88 public static void setup() { 89 sResponseThread = new HandlerThread("response"); 90 sResponseThread.start(); 91 sResponseHandler = new Handler(sResponseThread.getLooper()); 92 93 sContext = InstrumentationRegistry.getInstrumentation().getContext(); 94 sPm = sContext.getPackageManager(); 95 } 96 97 @AfterClass tearDown()98 public static void tearDown() { 99 sResponseThread.quit(); 100 } 101 sendCommand(@onNull String sourcePackageName, @Nullable String targetPackageName, int targetUid, @Nullable Parcelable intentExtra, String action, boolean waitForReady)102 Result sendCommand(@NonNull String sourcePackageName, @Nullable String targetPackageName, 103 int targetUid, @Nullable Parcelable intentExtra, String action, boolean waitForReady) 104 throws Exception { 105 final Intent intent = new Intent(action) 106 .setComponent(new ComponentName(sourcePackageName, ACTIVITY_CLASS_TEST)) 107 // data uri unique to each activity start to ensure actual launch and not just 108 // redisplay 109 .setData(Uri.parse("test://" + UUID.randomUUID().toString())) 110 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 111 if (targetPackageName != null) { 112 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPackageName); 113 } 114 if (targetUid > INVALID_UID) { 115 intent.putExtra(Intent.EXTRA_UID, targetUid); 116 } 117 if (intentExtra != null) { 118 if (intentExtra instanceof Intent) { 119 intent.putExtra(Intent.EXTRA_INTENT, intentExtra); 120 } else if (intentExtra instanceof PendingIntent) { 121 intent.putExtra(EXTRA_PENDING_INTENT, intentExtra); 122 } else if (intentExtra instanceof Bundle) { 123 intent.putExtra(EXTRA_DATA, intentExtra); 124 } 125 } 126 127 final ConditionVariable latch = new ConditionVariable(); 128 final AtomicReference<Bundle> resultReference = new AtomicReference<>(); 129 final RemoteCallback callback = new RemoteCallback( 130 bundle -> { 131 if (bundle != null && bundle.containsKey(EXTRA_SEGMENT_RESULT) 132 && bundle.containsKey(EXTRA_SEGMENT_SIZE)) { 133 // Receive AppWidgetProviderInfo list in segments. 134 Bundle resultReferenceBundle = resultReference.get(); 135 ArrayList<Parcelable> segmentParcelables = 136 bundle.getParcelableArrayList(EXTRA_SEGMENT_RESULT); 137 if (resultReferenceBundle != null) { 138 // Splice the received data into parcelables. 139 ArrayList<Parcelable> parcelables = resultReferenceBundle 140 .getParcelableArrayList(Intent.EXTRA_RETURN_RESULT); 141 if (parcelables != null) { 142 parcelables.addAll(segmentParcelables); 143 } else { 144 parcelables = segmentParcelables; 145 } 146 // Place the assembled list into bundle. 147 bundle.putParcelableArrayList(Intent.EXTRA_RETURN_RESULT, parcelables); 148 } else { 149 // No need to assemble segment data the first time they are received. 150 bundle.putParcelableArrayList(Intent.EXTRA_RETURN_RESULT, 151 segmentParcelables); 152 } 153 resultReference.set(bundle); 154 if (bundle.getParcelableArrayList(Intent.EXTRA_RETURN_RESULT) == null 155 || bundle.getParcelableArrayList(Intent.EXTRA_RETURN_RESULT).size() 156 >= bundle.getShort(EXTRA_SEGMENT_SIZE)) { 157 // Continue the testing process when the number of 158 // received items reaches the expected number. 159 latch.open(); 160 } 161 } else { 162 // Follow the default process when receiving other data. 163 resultReference.set(bundle); 164 latch.open(); 165 } 166 }, 167 sResponseHandler); 168 intent.putExtra(EXTRA_REMOTE_CALLBACK, callback); 169 if (waitForReady) { 170 AmUtils.waitForBroadcastBarrier(); 171 startAndWaitForCommandReady(intent); 172 } else { 173 final ActivityOptions options = ActivityOptions.makeBasic(); 174 options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN); 175 sContext.startActivity(intent, options.toBundle()); 176 } 177 return () -> { 178 final long latchTimeout = 179 waitForReady ? DEFAULT_TIMEOUT_WAIT_FOR_READY_MS : DEFAULT_TIMEOUT_MS; 180 if (!latch.block(latchTimeout)) { 181 throw new TimeoutException( 182 "Latch timed out while awaiting a response from " + sourcePackageName 183 + " after " + latchTimeout + "ms"); 184 } 185 final Bundle bundle = resultReference.get(); 186 if (bundle != null && bundle.containsKey(EXTRA_ERROR)) { 187 throw Objects.requireNonNull(bundle.getSerializable(EXTRA_ERROR, Exception.class)); 188 } 189 return bundle; 190 }; 191 } 192 startAndWaitForCommandReady(Intent intent)193 private void startAndWaitForCommandReady(Intent intent) throws Exception { 194 final ConditionVariable latchForReady = new ConditionVariable(); 195 final RemoteCallback readyCallback = new RemoteCallback(bundle -> latchForReady.open(), 196 sResponseHandler); 197 intent.putExtra(EXTRA_REMOTE_READY_CALLBACK, readyCallback); 198 final ActivityOptions options = ActivityOptions.makeBasic(); 199 options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN); 200 sContext.startActivity(intent, options.toBundle()); 201 if (!latchForReady.block(DEFAULT_TIMEOUT_MS)) { 202 throw new TimeoutException( 203 "Latch timed out while awaiting a response from command " + intent.getAction()); 204 } 205 } 206 sendCommandBlocking(@onNull String sourcePackageName, @Nullable String targetPackageName, @Nullable Parcelable intentExtra, String action)207 Bundle sendCommandBlocking(@NonNull String sourcePackageName, 208 @Nullable String targetPackageName, @Nullable Parcelable intentExtra, String action) 209 throws Exception { 210 final Result result = sendCommand(sourcePackageName, targetPackageName, 211 INVALID_UID /* targetUid */, intentExtra, action, false /* waitForReady */); 212 return result.await(); 213 } 214 sendCommandBlocking(@onNull String sourcePackageName, int targetUid, @Nullable Parcelable intentExtra, String action)215 Bundle sendCommandBlocking(@NonNull String sourcePackageName, int targetUid, 216 @Nullable Parcelable intentExtra, String action) 217 throws Exception { 218 final Result result = sendCommand(sourcePackageName, null /* targetPackageName */, 219 targetUid, intentExtra, action, false /* waitForReady */); 220 return result.await(); 221 } 222 assertVisible(String sourcePackageName, String targetPackageName, ThrowingFunction<String, String[]> commandMethod)223 void assertVisible(String sourcePackageName, String targetPackageName, 224 ThrowingFunction<String, String[]> commandMethod) throws Exception { 225 final String[] packageNames = commandMethod.apply(sourcePackageName); 226 assertThat("The list of package names should not be null", 227 packageNames, notNullValue()); 228 assertThat(sourcePackageName + " should be able to see " + targetPackageName, 229 packageNames, hasItemInArray(targetPackageName)); 230 } 231 assertNotVisible(String sourcePackageName, String targetPackageName, ThrowingFunction<String, String[]> commandMethod)232 void assertNotVisible(String sourcePackageName, String targetPackageName, 233 ThrowingFunction<String, String[]> commandMethod) throws Exception { 234 final String[] packageNames = commandMethod.apply(sourcePackageName); 235 assertThat("The list of package names should not be null", 236 packageNames, notNullValue()); 237 assertThat(sourcePackageName + " should not be able to see " + targetPackageName, 238 packageNames, not(hasItemInArray(targetPackageName))); 239 } 240 assertVisible(String sourcePackageName, String targetPackageName, ThrowingBiFunction<String, String, String[]> commandMethod)241 void assertVisible(String sourcePackageName, String targetPackageName, 242 ThrowingBiFunction<String, String, String[]> commandMethod) throws Exception { 243 final String[] packageNames = commandMethod.apply(sourcePackageName, targetPackageName); 244 assertThat(sourcePackageName + " should be able to see " + targetPackageName, 245 packageNames, hasItemInArray(targetPackageName)); 246 } 247 assertNotVisible(String sourcePackageName, String targetPackageName, ThrowingBiFunction<String, String, String[]> commandMethod)248 void assertNotVisible(String sourcePackageName, String targetPackageName, 249 ThrowingBiFunction<String, String, String[]> commandMethod) throws Exception { 250 final String[] packageNames = commandMethod.apply(sourcePackageName, targetPackageName); 251 assertThat(sourcePackageName + " should not be able to see " + targetPackageName, 252 packageNames, not(hasItemInArray(targetPackageName))); 253 } 254 getSessionInfos(String action, String sourcePackageName, int sessionId)255 Integer[] getSessionInfos(String action, String sourcePackageName, int sessionId) 256 throws Exception { 257 final Bundle extraData = new Bundle(); 258 extraData.putInt(EXTRA_ID, sessionId); 259 final Bundle response = sendCommandBlocking(sourcePackageName, null /* targetPackageName */, 260 extraData, action); 261 final List<PackageInstaller.SessionInfo> infos = response.getParcelableArrayList( 262 Intent.EXTRA_RETURN_RESULT, PackageInstaller.SessionInfo.class); 263 return infos.stream() 264 .map(i -> (i == null ? PackageInstaller.SessionInfo.INVALID_ID : i.getSessionId())) 265 .distinct() 266 .toArray(Integer[]::new); 267 } 268 } 269