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 android.content.pm.cts; 18 19 import static android.app.PendingIntent.FLAG_MUTABLE; 20 import static android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL; 21 import static android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES; 22 import static android.content.pm.PackageManager.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES; 23 24 import static com.google.common.truth.Truth.assertThat; 25 26 import static org.junit.Assert.assertThrows; 27 28 import android.app.PendingIntent; 29 import android.content.BroadcastReceiver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.IntentSender; 34 import android.content.SharedPreferences; 35 import android.content.pm.PackageInfo; 36 import android.content.pm.PackageInstaller; 37 import android.content.pm.PackageInstaller.Session; 38 import android.content.pm.PackageInstaller.SessionParams; 39 import android.content.pm.PackageManager; 40 import android.content.pm.SharedLibraryInfo; 41 import android.content.pm.Signature; 42 import android.content.pm.SigningInfo; 43 import android.content.pm.dependencyinstaller.DependencyInstallerCallback; 44 import android.content.pm.dependencyinstaller.DependencyInstallerService; 45 import android.os.Handler; 46 import android.os.Looper; 47 import android.os.SystemClock; 48 import android.os.UserHandle; 49 import android.preference.PreferenceManager; 50 import android.util.Log; 51 import android.util.PackageUtils; 52 53 import androidx.annotation.NonNull; 54 import androidx.annotation.Nullable; 55 import androidx.test.InstrumentationRegistry; 56 57 import libcore.util.HexEncoding; 58 59 import java.io.FileInputStream; 60 import java.io.IOException; 61 import java.io.InputStream; 62 import java.io.OutputStream; 63 import java.util.ArrayList; 64 import java.util.List; 65 import java.util.concurrent.CountDownLatch; 66 import java.util.concurrent.TimeUnit; 67 68 /* 69 * A DependencyInstallerService for test. 70 * 71 * The behavior of the service is controlled by the test by passing specific method names via 72 * SharedPreferences. 73 * 74 */ 75 public class TestDependencyInstallerService extends DependencyInstallerService { 76 77 private static final String TAG = TestDependencyInstallerService.class.getSimpleName(); 78 private static final String LIB_NAME_SDK_1 = "com.test.sdk1"; 79 private static final String LIB_NAME_SDK_2 = "com.test.sdk2"; 80 private static final String TEST_APK_PATH = "/data/local/tmp/cts/content/"; 81 private static final String TEST_SDK1_APK_NAME = "HelloWorldSdk1"; 82 private static final String TEST_SDK2_APK_NAME = "HelloWorldSdk2"; 83 private static final int WAIT_FOR_INSTALL_MS = 60 * 1000; 84 85 static final String METHOD_NAME = TAG + "-method-name"; 86 static final String ERROR_MESSAGE = TAG + "-error-message"; 87 static final String METHOD_INSTALL_SYNC = TAG + "-install-sync"; 88 static final String METHOD_INSTALL_ASYNC = TAG + "-install-async"; 89 static final String METHOD_INVALID_SESSION_ID = TAG + "-invalid-session-id"; 90 static final String METHOD_ABANDONED_SESSION_ID = TAG + "-abandoned-session-id"; 91 static final String METHOD_ABANDON_SESSION_DURING_INSTALL = TAG 92 + "-abandon-session-during-install"; 93 static final String METHOD_RESUME_ON_FAILURE_FAIL_INSTALL = TAG 94 + "-resume-on-failure-fail-install"; 95 static final String METHOD_VERIFY_USER_ID = TAG + "-verify-user-id"; 96 97 private String mCertDigest; 98 99 @Override onDependenciesRequired(List<SharedLibraryInfo> neededLibraries, DependencyInstallerCallback callback)100 public void onDependenciesRequired(List<SharedLibraryInfo> neededLibraries, 101 DependencyInstallerCallback callback) { 102 103 String methodName = getMethodName(); 104 int userId = UserHandle.myUserId(); 105 Log.d(TAG, "onDependenciesRequired call received: " + methodName + " for user: " + userId); 106 107 try { 108 109 if (!isInstrumented()) { 110 // Test app is not instrumented when we bind to it on a different user. 111 112 // For multi-user test, methodName is always empty since it's written in 113 // SharedPreferences storage of a different user. So we opt to install synchronously 114 // all the time. 115 installDependenciesSync(neededLibraries, callback); 116 return; 117 } 118 119 // All CTS test artifacts are signed with same cert. So we can assume the SDK apks 120 // we have will be same cert as our current package. 121 mCertDigest = getPackageCertDigest(getContext().getPackageName()); 122 validateNeededLibraries(neededLibraries); 123 124 if (methodName.equals(METHOD_INSTALL_SYNC)) { 125 installDependenciesSync(neededLibraries, callback); 126 } else if (methodName.equals(METHOD_INSTALL_ASYNC)) { 127 installDependenciesAsync(neededLibraries, callback); 128 } else if (methodName.equals(METHOD_INVALID_SESSION_ID)) { 129 testInvalidSessionId(callback); 130 } else if (methodName.equals(METHOD_ABANDONED_SESSION_ID)) { 131 testAbandonedSessionId(callback); 132 } else if (methodName.equals(METHOD_ABANDON_SESSION_DURING_INSTALL)) { 133 testAbandonSessionDuringInstall(neededLibraries, callback); 134 } else if (methodName.equals(METHOD_VERIFY_USER_ID)) { 135 installDependenciesSync(neededLibraries, callback); 136 } else if (methodName.equals(METHOD_RESUME_ON_FAILURE_FAIL_INSTALL)) { 137 testResumeOnFailureFailsInstall(neededLibraries, callback); 138 } else { 139 throw new IllegalStateException("Unknown method name: " + methodName); 140 } 141 } catch (Throwable e) { 142 Log.w(TAG, e.getMessage(), e); 143 setErrorMessage(e.getMessage()); 144 try { 145 callback.onFailureToResolveAllDependencies(); 146 } catch (Exception e2) { 147 Log.w(TAG, e2.getMessage()); 148 } 149 } 150 } 151 isSdk1(SharedLibraryInfo info)152 private boolean isSdk1(SharedLibraryInfo info) { 153 if (info.getCertDigests().isEmpty() || info.getName() == null) { 154 return false; 155 } 156 return info.getName().equals(LIB_NAME_SDK_1) 157 && info.getCertDigests().get(0).equals(mCertDigest); 158 } 159 isSdk2(SharedLibraryInfo info)160 private boolean isSdk2(SharedLibraryInfo info) { 161 if (info.getCertDigests().isEmpty() || info.getName() == null) { 162 return false; 163 } 164 return info.getName().equals(LIB_NAME_SDK_2) 165 && info.getCertDigests().get(0).equals(mCertDigest); 166 } 167 168 /** 169 * Send a non-existing session-id to system. 170 */ testInvalidSessionId(DependencyInstallerCallback callback)171 private void testInvalidSessionId(DependencyInstallerCallback callback) throws Exception { 172 173 // Pass a session id that doesn't exist 174 IllegalArgumentException exception = 175 assertThrows( 176 IllegalArgumentException.class, 177 () -> { 178 callback.onAllDependenciesResolved(new int[] {100}); 179 }); 180 181 assertThat(exception).hasMessageThat().contains("Failed to find session: 100"); 182 183 // Fail the resolution to resume the original install flow. 184 callback.onFailureToResolveAllDependencies(); 185 } 186 187 /** 188 * Send a session id that has already been abandoned. 189 */ testAbandonedSessionId(DependencyInstallerCallback callback)190 private void testAbandonedSessionId(DependencyInstallerCallback callback) throws Exception { 191 192 SessionParams params = new SessionParams(MODE_FULL_INSTALL); 193 PackageInstaller installer = getPackageManager().getPackageInstaller(); 194 int sessionId = installer.createSession(params); 195 // Register a listener for this session id 196 SessionListener sessionListener = new SessionListener(sessionId); 197 try { 198 getContext().getPackageManager().getPackageInstaller().registerSessionCallback( 199 sessionListener, 200 new Handler(Looper.getMainLooper())); 201 202 // Abandon the session 203 Session session = installer.openSession(sessionId); 204 session.abandon(); 205 206 // Wait for session to finish 207 sessionListener.latch.await(5, TimeUnit.SECONDS); 208 } finally { 209 getContext().getPackageManager().getPackageInstaller().unregisterSessionCallback( 210 sessionListener); 211 } 212 213 // Pass an abandoned session id 214 IllegalArgumentException exception = 215 assertThrows( 216 IllegalArgumentException.class, 217 () -> { 218 callback.onAllDependenciesResolved(new int[] {sessionId}); 219 }); 220 221 assertThat(exception).hasMessageThat().contains("Session already finished: " + sessionId); 222 223 // Fail the resolution to resume the original install flow. 224 callback.onFailureToResolveAllDependencies(); 225 } 226 testAbandonSessionDuringInstall(List<SharedLibraryInfo> neededLibraries, DependencyInstallerCallback callback)227 private void testAbandonSessionDuringInstall(List<SharedLibraryInfo> neededLibraries, 228 DependencyInstallerCallback callback) throws Exception { 229 230 assertThat(neededLibraries).hasSize(1); 231 SharedLibraryInfo info = neededLibraries.get(0); 232 233 // Create two sessions and have system wait for them 234 List<Integer> sessionIds = createSessionIds(2); 235 callback.onAllDependenciesResolved(toIntArray(sessionIds)); 236 237 // Now we commit the first session and then abandon the second. The system should 238 // be able to detect session being abandoned and stop waiting. 239 240 int firstSession = sessionIds.get(0); 241 SyncBroadcastReceiver sender = new SyncBroadcastReceiver(List.of(firstSession)); 242 Session session = writeToSession(info, firstSession); 243 session.commit(sender.getIntentSender(this)); 244 assertThat(sender.latch.await(WAIT_FOR_INSTALL_MS, TimeUnit.MILLISECONDS)).isTrue(); 245 assertThat(sender.isFailure).isFalse(); 246 247 // Now hat first session has finished, abandon the second session 248 int secondSession = sessionIds.get(1); 249 session = writeToSession(info, secondSession); 250 session.abandon(); 251 } 252 testResumeOnFailureFailsInstall(List<SharedLibraryInfo> neededLibraries, DependencyInstallerCallback callback)253 private void testResumeOnFailureFailsInstall(List<SharedLibraryInfo> neededLibraries, 254 DependencyInstallerCallback callback) throws Exception { 255 256 // Create a session and have system wait for them 257 List<Integer> sessionIds = createSessionIds(1); 258 callback.onAllDependenciesResolved(toIntArray(sessionIds)); 259 260 // Now we commit the session without writing to it. This ensures it will fail installation. 261 int firstSession = sessionIds.get(0); 262 SyncBroadcastReceiver sender = new SyncBroadcastReceiver(List.of(firstSession)); 263 Session session = writeToSession(null, firstSession); 264 session.commit(sender.getIntentSender(this)); 265 } 266 installDependenciesSync(List<SharedLibraryInfo> neededLibraries, DependencyInstallerCallback callback)267 private void installDependenciesSync(List<SharedLibraryInfo> neededLibraries, 268 DependencyInstallerCallback callback) throws Exception { 269 270 int size = neededLibraries.size(); 271 List<Integer> sessionIds = createSessionIds(neededLibraries.size()); 272 273 SyncBroadcastReceiver sender = new SyncBroadcastReceiver(sessionIds); 274 for (int i = 0; i < size; i++) { 275 int sessionId = sessionIds.get(i); 276 SharedLibraryInfo info = neededLibraries.get(i); 277 Session session = writeToSession(info, sessionId); 278 session.commit(sender.getIntentSender(this)); 279 } 280 281 // Wait for all sessions to finish installation 282 assertThat(sender.latch.await(WAIT_FOR_INSTALL_MS, TimeUnit.MILLISECONDS)).isTrue(); 283 if (sender.isFailure) { 284 callback.onFailureToResolveAllDependencies(); 285 } else { 286 callback.onAllDependenciesResolved(toIntArray(sessionIds)); 287 } 288 } 289 installDependenciesAsync(List<SharedLibraryInfo> neededLibraries, DependencyInstallerCallback callback)290 private void installDependenciesAsync(List<SharedLibraryInfo> neededLibraries, 291 DependencyInstallerCallback callback) throws Exception { 292 293 int size = neededLibraries.size(); 294 List<Integer> sessionIds = createSessionIds(neededLibraries.size()); 295 296 // Return the session ids immediately 297 callback.onAllDependenciesResolved(toIntArray(sessionIds)); 298 299 SyncBroadcastReceiver sender = new SyncBroadcastReceiver(sessionIds); 300 for (int i = 0; i < size; i++) { 301 int sessionId = sessionIds.get(i); 302 SharedLibraryInfo info = neededLibraries.get(i); 303 Session session = writeToSession(info, sessionId); 304 session.commit(sender.getIntentSender(this)); 305 } 306 } 307 createSessionIds(int size)308 private List<Integer> createSessionIds(int size) throws Exception { 309 PackageInstaller installer = getPackageManager().getPackageInstaller(); 310 List<Integer> sessionIds = new ArrayList<>(); 311 for (int i = 0; i < size; i++) { 312 SessionParams params = new SessionParams(MODE_FULL_INSTALL); 313 int sessionId = installer.createSession(params); 314 Log.i(TAG, "Session created: " + sessionId); 315 sessionIds.add(sessionId); 316 } 317 return sessionIds; 318 } 319 writeToSession(@ullable SharedLibraryInfo info, int sessionId)320 private Session writeToSession(@Nullable SharedLibraryInfo info, int sessionId) 321 throws Exception { 322 PackageInstaller installer = getPackageManager().getPackageInstaller(); 323 Session session = installer.openSession(sessionId); 324 if (info == null) { 325 return session; 326 } 327 if (info.getName().equals(LIB_NAME_SDK_1)) { 328 writeApk(session, TEST_SDK1_APK_NAME); 329 } else if (info.getName().equals(LIB_NAME_SDK_2)) { 330 writeApk(session, TEST_SDK2_APK_NAME); 331 } 332 return session; 333 } 334 validateNeededLibraries(List<SharedLibraryInfo> neededLibraries)335 private void validateNeededLibraries(List<SharedLibraryInfo> neededLibraries) throws Exception { 336 for (SharedLibraryInfo info: neededLibraries) { 337 if (isSdk1(info)) { 338 Log.i(TAG, "SDK1 missing dependency found"); 339 continue; 340 } 341 342 if (isSdk2(info)) { 343 Log.i(TAG, "SDK2 missing dependency found"); 344 continue; 345 } 346 // For everything else, fail. 347 throw new IllegalStateException("Unsupported SDK found: " + info.getName() + " " 348 + info.getCertDigests().get(0)); 349 } 350 } 351 getMethodName()352 private String getMethodName() { 353 return getDefaultSharedPreferences().getString(METHOD_NAME, ""); 354 } 355 setErrorMessage(String msg)356 private void setErrorMessage(String msg) { 357 getDefaultSharedPreferences().edit().putString(ERROR_MESSAGE, msg).commit(); 358 } 359 writeApk(@onNull Session session, @NonNull String name)360 private void writeApk(@NonNull Session session, @NonNull String name) throws IOException { 361 String apkPath = createApkPath(name); 362 try (InputStream in = new FileInputStream(apkPath)) { 363 try (OutputStream out = session.openWrite(name, 0, -1)) { 364 copyStream(in, out); 365 } 366 } 367 } 368 getPackageCertDigest(String packageName)369 private String getPackageCertDigest(String packageName) throws Exception { 370 PackageInfo sdkPackageInfo = getPackageManager().getPackageInfo(packageName, 371 PackageManager.PackageInfoFlags.of( 372 GET_SIGNING_CERTIFICATES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES)); 373 SigningInfo signingInfo = sdkPackageInfo.signingInfo; 374 Signature[] signatures = 375 signingInfo != null ? signingInfo.getSigningCertificateHistory() : null; 376 byte[] digest = PackageUtils.computeSha256DigestBytes(signatures[0].toByteArray()); 377 return new String(HexEncoding.encode(digest)); 378 } 379 isInstrumented()380 private boolean isInstrumented() throws Exception { 381 try { 382 InstrumentationRegistry.getContext(); 383 return true; 384 } catch (IllegalStateException e) { 385 if (e.getMessage().contains("No instrumentation registered!")) { 386 return false; 387 } 388 throw e; 389 } 390 } 391 392 private static class SyncBroadcastReceiver extends BroadcastReceiver { 393 private final int[] mSessionIds; 394 private int mPendingSessionCount; 395 396 public final CountDownLatch latch = new CountDownLatch(1); 397 public boolean isFailure = false; 398 SyncBroadcastReceiver(List<Integer> sessionIds)399 SyncBroadcastReceiver(List<Integer> sessionIds) { 400 mSessionIds = toIntArray(sessionIds); 401 mPendingSessionCount = sessionIds.size(); 402 } 403 404 @Override onReceive(Context context, Intent intent)405 public void onReceive(Context context, Intent intent) { 406 Log.i(TAG, "Received intent " + prettyPrint(intent)); 407 int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 408 PackageInstaller.STATUS_FAILURE); 409 if (status != PackageInstaller.STATUS_SUCCESS) { 410 isFailure = true; 411 } 412 synchronized (this) { 413 mPendingSessionCount--; 414 if (mPendingSessionCount == 0) { 415 latch.countDown(); 416 } 417 } 418 } 419 getIntentSender(Context context)420 public IntentSender getIntentSender(Context context) { 421 String action = TestDependencyInstallerService.class.getName() 422 + SystemClock.elapsedRealtime(); 423 context.registerReceiver(this, new IntentFilter(action), 424 Context.RECEIVER_EXPORTED); 425 Intent intent = new Intent(action).setPackage(context.getPackageName()) 426 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 427 PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, FLAG_MUTABLE); 428 return pending.getIntentSender(); 429 } 430 prettyPrint(Intent intent)431 private static String prettyPrint(Intent intent) { 432 int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1); 433 int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 434 PackageInstaller.STATUS_FAILURE); 435 String message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); 436 return String.format("%s: {\n" 437 + "sessionId = %d\n" 438 + "status = %d\n" 439 + "message = %s\n" 440 + "}", intent, sessionId, status, message); 441 } 442 } 443 444 static class SessionListener extends PackageInstaller.SessionCallback { 445 446 public final CountDownLatch latch = new CountDownLatch(1); 447 448 private final int mSessionId; 449 SessionListener(int sessionId)450 SessionListener(int sessionId) { 451 mSessionId = sessionId; 452 } 453 454 @Override onCreated(int sessionId)455 public void onCreated(int sessionId) { 456 } 457 458 @Override onBadgingChanged(int sessionId)459 public void onBadgingChanged(int sessionId) { 460 } 461 462 @Override onActiveChanged(int sessionId, boolean active)463 public void onActiveChanged(int sessionId, boolean active) { 464 } 465 466 @Override onProgressChanged(int sessionId, float progress)467 public void onProgressChanged(int sessionId, float progress) { 468 } 469 470 @Override onFinished(int sessionId, boolean success)471 public void onFinished(int sessionId, boolean success) { 472 if (sessionId == mSessionId) { 473 latch.countDown(); 474 } 475 } 476 } 477 createApkPath(String baseName)478 private static String createApkPath(String baseName) { 479 return TEST_APK_PATH + baseName + ".apk"; 480 } 481 getDefaultSharedPreferences()482 private SharedPreferences getDefaultSharedPreferences() { 483 final Context appContext = getContext().getApplicationContext(); 484 return PreferenceManager.getDefaultSharedPreferences(appContext); 485 } 486 getContext()487 private Context getContext() { 488 return this; 489 } 490 toIntArray(List<Integer> list)491 private static int[] toIntArray(List<Integer> list) { 492 return list.stream().mapToInt(i->i).toArray(); 493 } 494 copyStream(InputStream in, OutputStream out)495 private static void copyStream(InputStream in, OutputStream out) throws IOException { 496 int total = 0; 497 byte[] buffer = new byte[8192]; 498 int c; 499 while ((c = in.read(buffer)) != -1) { 500 total += c; 501 out.write(buffer, 0, c); 502 } 503 } 504 505 } 506