• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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