• 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 com.android.server.appfunctions;
18 
19 import static android.app.appfunctions.AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.WorkerThread;
24 import android.app.appfunctions.AppFunctionRuntimeMetadata;
25 import android.app.appfunctions.AppFunctionStaticMetadataHelper;
26 import android.app.appsearch.AppSearchBatchResult;
27 import android.app.appsearch.AppSearchManager;
28 import android.app.appsearch.AppSearchManager.SearchContext;
29 import android.app.appsearch.AppSearchResult;
30 import android.app.appsearch.AppSearchSchema;
31 import android.app.appsearch.PackageIdentifier;
32 import android.app.appsearch.PropertyPath;
33 import android.app.appsearch.PutDocumentsRequest;
34 import android.app.appsearch.RemoveByDocumentIdRequest;
35 import android.app.appsearch.SearchResult;
36 import android.app.appsearch.SearchSpec;
37 import android.app.appsearch.SetSchemaRequest;
38 import android.content.pm.PackageInfo;
39 import android.content.pm.PackageManager;
40 import android.content.pm.Signature;
41 import android.util.ArrayMap;
42 import android.util.ArraySet;
43 import android.util.Slog;
44 
45 import com.android.internal.annotations.GuardedBy;
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.infra.AndroidFuture;
48 
49 import java.security.MessageDigest;
50 import java.security.NoSuchAlgorithmException;
51 import java.util.Collection;
52 import java.util.List;
53 import java.util.Objects;
54 import java.util.Set;
55 import java.util.concurrent.ExecutionException;
56 import java.util.concurrent.ExecutorService;
57 import java.util.concurrent.Executors;
58 import java.util.concurrent.Future;
59 
60 /**
61  * This class implements helper methods for synchronously interacting with AppSearch while
62  * synchronizing AppFunction runtime and static metadata.
63  *
64  * <p>This class is not thread safe.
65  */
66 public class MetadataSyncAdapter {
67     private static final String TAG = MetadataSyncAdapter.class.getSimpleName();
68 
69     private final ExecutorService mExecutor;
70 
71     private final AppSearchManager mAppSearchManager;
72     private final PackageManager mPackageManager;
73     private final Object mLock = new Object();
74 
75     @GuardedBy("mLock")
76     private Future<?> mCurrentSyncTask;
77 
78     // Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility
79     // by permissions.
80     public static final int EXECUTE_APP_FUNCTIONS = 9;
81 
MetadataSyncAdapter( @onNull PackageManager packageManager, @NonNull AppSearchManager appSearchManager)82     public MetadataSyncAdapter(
83             @NonNull PackageManager packageManager, @NonNull AppSearchManager appSearchManager) {
84         mPackageManager = Objects.requireNonNull(packageManager);
85         mAppSearchManager = Objects.requireNonNull(appSearchManager);
86         mExecutor =
87                 Executors.newSingleThreadExecutor(
88                         new NamedThreadFactory("AppFunctionSyncExecutors"));
89     }
90 
91     /**
92      * This method submits a request to synchronize the AppFunction runtime and static metadata.
93      *
94      * @return A {@link AndroidFuture} that completes with a boolean value indicating whether the
95      *     synchronization was successful.
96      */
submitSyncRequest()97     public AndroidFuture<Boolean> submitSyncRequest() {
98         SearchContext staticMetadataSearchContext =
99                 new SearchContext.Builder(
100                                 AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)
101                         .build();
102         SearchContext runtimeMetadataSearchContext =
103                 new SearchContext.Builder(
104                                 AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB)
105                         .build();
106         AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>();
107         Runnable runnable =
108                 () -> {
109                     try (FutureAppSearchSession staticMetadataSearchSession =
110                                     new FutureAppSearchSessionImpl(
111                                             mAppSearchManager,
112                                             AppFunctionExecutors.THREAD_POOL_EXECUTOR,
113                                             staticMetadataSearchContext);
114                             FutureAppSearchSession runtimeMetadataSearchSession =
115                                     new FutureAppSearchSessionImpl(
116                                             mAppSearchManager,
117                                             AppFunctionExecutors.THREAD_POOL_EXECUTOR,
118                                             runtimeMetadataSearchContext)) {
119 
120                         trySyncAppFunctionMetadataBlocking(
121                                 staticMetadataSearchSession, runtimeMetadataSearchSession);
122                         settableSyncStatus.complete(true);
123 
124                     } catch (Exception ex) {
125                         settableSyncStatus.completeExceptionally(ex);
126                     }
127                 };
128 
129         synchronized (mLock) {
130             if (mCurrentSyncTask != null && !mCurrentSyncTask.isDone()) {
131                 var unused = mCurrentSyncTask.cancel(false);
132             }
133             mCurrentSyncTask = mExecutor.submit(runnable);
134         }
135 
136         return settableSyncStatus;
137     }
138 
139     /** This method shuts down the {@link MetadataSyncAdapter} scheduler. */
shutDown()140     public void shutDown() {
141         mExecutor.shutdown();
142     }
143 
144     @WorkerThread
145     @VisibleForTesting
trySyncAppFunctionMetadataBlocking( @onNull FutureAppSearchSession staticMetadataSearchSession, @NonNull FutureAppSearchSession runtimeMetadataSearchSession)146     void trySyncAppFunctionMetadataBlocking(
147             @NonNull FutureAppSearchSession staticMetadataSearchSession,
148             @NonNull FutureAppSearchSession runtimeMetadataSearchSession)
149             throws ExecutionException, InterruptedException {
150         ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap =
151                 getPackageToFunctionIdMap(
152                         staticMetadataSearchSession,
153                         AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE,
154                         AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID,
155                         AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME);
156         ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap =
157                 getPackageToFunctionIdMap(
158                         runtimeMetadataSearchSession,
159                         RUNTIME_SCHEMA_TYPE,
160                         AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID,
161                         AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME);
162 
163         ArrayMap<String, ArraySet<String>> addedFunctionsDiffMap =
164                 getAddedFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
165         ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap =
166                 getRemovedFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
167 
168         if (!staticPackageToFunctionMap.keySet().equals(runtimePackageToFunctionMap.keySet())) {
169             // Drop removed packages from removedFunctionsDiffMap, as setSchema() deletes them
170             ArraySet<String> removedPackages =
171                     getRemovedPackages(
172                             staticPackageToFunctionMap.keySet(), removedFunctionsDiffMap.keySet());
173             for (String packageName : removedPackages) {
174                 removedFunctionsDiffMap.remove(packageName);
175             }
176             Set<AppSearchSchema> appRuntimeMetadataSchemas =
177                     getAllRuntimeMetadataSchemas(staticPackageToFunctionMap.keySet());
178             appRuntimeMetadataSchemas.add(
179                     AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema());
180             SetSchemaRequest addSetSchemaRequest =
181                     buildSetSchemaRequestForRuntimeMetadataSchemas(
182                             mPackageManager, appRuntimeMetadataSchemas);
183             Objects.requireNonNull(
184                     runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
185         }
186 
187         if (!removedFunctionsDiffMap.isEmpty()) {
188             RemoveByDocumentIdRequest removeByDocumentIdRequest =
189                     buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap);
190             AppSearchBatchResult<String, Void> removeDocumentBatchResult =
191                     runtimeMetadataSearchSession.remove(removeByDocumentIdRequest).get();
192             if (!removeDocumentBatchResult.isSuccess()) {
193                 throw convertFailedAppSearchResultToException(
194                         removeDocumentBatchResult.getFailures().values());
195             }
196         }
197 
198         if (!addedFunctionsDiffMap.isEmpty()) {
199             PutDocumentsRequest putDocumentsRequest =
200                     buildPutRuntimeMetadataRequest(addedFunctionsDiffMap);
201             AppSearchBatchResult<String, Void> putDocumentBatchResult =
202                     runtimeMetadataSearchSession.put(putDocumentsRequest).get();
203             if (!putDocumentBatchResult.isSuccess()) {
204                 throw convertFailedAppSearchResultToException(
205                         putDocumentBatchResult.getFailures().values());
206             }
207         }
208     }
209 
210     @NonNull
convertFailedAppSearchResultToException( @onNull Collection<AppSearchResult<Void>> appSearchResult)211     private static IllegalStateException convertFailedAppSearchResultToException(
212             @NonNull Collection<AppSearchResult<Void>> appSearchResult) {
213         Objects.requireNonNull(appSearchResult);
214         StringBuilder errorMessages = new StringBuilder();
215         for (AppSearchResult<Void> result : appSearchResult) {
216             errorMessages.append(result.getErrorMessage());
217         }
218         return new IllegalStateException(errorMessages.toString());
219     }
220 
221     @NonNull
buildPutRuntimeMetadataRequest( @onNull ArrayMap<String, ArraySet<String>> addedFunctionsDiffMap)222     private PutDocumentsRequest buildPutRuntimeMetadataRequest(
223             @NonNull ArrayMap<String, ArraySet<String>> addedFunctionsDiffMap) {
224         Objects.requireNonNull(addedFunctionsDiffMap);
225         PutDocumentsRequest.Builder putDocumentRequestBuilder = new PutDocumentsRequest.Builder();
226 
227         for (int i = 0; i < addedFunctionsDiffMap.size(); i++) {
228             String packageName = addedFunctionsDiffMap.keyAt(i);
229             ArraySet<String> addedFunctionIds = addedFunctionsDiffMap.valueAt(i);
230             for (String addedFunctionId : addedFunctionIds) {
231                 putDocumentRequestBuilder.addGenericDocuments(
232                         new AppFunctionRuntimeMetadata.Builder(packageName, addedFunctionId)
233                                 .build());
234             }
235         }
236         return putDocumentRequestBuilder.build();
237     }
238 
239     @NonNull
buildRemoveRuntimeMetadataRequest( @onNull ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap)240     private RemoveByDocumentIdRequest buildRemoveRuntimeMetadataRequest(
241             @NonNull ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap) {
242         Objects.requireNonNull(AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE);
243         Objects.requireNonNull(removedFunctionsDiffMap);
244         RemoveByDocumentIdRequest.Builder removeDocumentRequestBuilder =
245                 new RemoveByDocumentIdRequest.Builder(
246                         AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE);
247 
248         for (int i = 0; i < removedFunctionsDiffMap.size(); i++) {
249             String packageName = removedFunctionsDiffMap.keyAt(i);
250             ArraySet<String> removedFunctionIds = removedFunctionsDiffMap.valueAt(i);
251             for (String functionId : removedFunctionIds) {
252                 String documentId =
253                         AppFunctionRuntimeMetadata.getDocumentIdForAppFunction(
254                                 packageName, functionId);
255                 removeDocumentRequestBuilder.addIds(documentId);
256             }
257         }
258         return removeDocumentRequestBuilder.build();
259     }
260 
261     @NonNull
buildSetSchemaRequestForRuntimeMetadataSchemas( @onNull PackageManager packageManager, @NonNull Set<AppSearchSchema> metadataSchemaSet)262     private SetSchemaRequest buildSetSchemaRequestForRuntimeMetadataSchemas(
263             @NonNull PackageManager packageManager,
264             @NonNull Set<AppSearchSchema> metadataSchemaSet) {
265         Objects.requireNonNull(metadataSchemaSet);
266         SetSchemaRequest.Builder setSchemaRequestBuilder =
267                 new SetSchemaRequest.Builder().setForceOverride(true).addSchemas(metadataSchemaSet);
268 
269         for (AppSearchSchema runtimeMetadataSchema : metadataSchemaSet) {
270             String packageName =
271                     AppFunctionRuntimeMetadata.getPackageNameFromSchema(
272                             runtimeMetadataSchema.getSchemaType());
273             byte[] packageCert = getCertificate(packageManager, packageName);
274             if (packageCert == null) {
275                 continue;
276             }
277             setSchemaRequestBuilder.setSchemaTypeVisibilityForPackage(
278                     runtimeMetadataSchema.getSchemaType(),
279                     true,
280                     new PackageIdentifier(packageName, packageCert));
281             setSchemaRequestBuilder.addRequiredPermissionsForSchemaTypeVisibility(
282                     runtimeMetadataSchema.getSchemaType(), Set.of(EXECUTE_APP_FUNCTIONS));
283         }
284         return setSchemaRequestBuilder.build();
285     }
286 
287     @NonNull
288     @WorkerThread
getAllRuntimeMetadataSchemas( @onNull Set<String> staticMetadataPackages)289     private Set<AppSearchSchema> getAllRuntimeMetadataSchemas(
290             @NonNull Set<String> staticMetadataPackages) {
291         Objects.requireNonNull(staticMetadataPackages);
292 
293         Set<AppSearchSchema> appRuntimeMetadataSchemas = new ArraySet<>();
294         for (String packageName : staticMetadataPackages) {
295             appRuntimeMetadataSchemas.add(
296                     AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(packageName));
297         }
298 
299         return appRuntimeMetadataSchemas;
300     }
301 
302     /**
303      * This method returns a set of packages that are in the removed function packages but not in
304      * the all existing static packages.
305      *
306      * @param allExistingStaticPackages A set of all existing static metadata packages.
307      * @param removedFunctionPackages A set of all removed function packages.
308      * @return A set of packages that are in the removed function packages but not in the all
309      *     existing static packages.
310      */
311     @NonNull
getRemovedPackages( @onNull Set<String> allExistingStaticPackages, @NonNull Set<String> removedFunctionPackages)312     private static ArraySet<String> getRemovedPackages(
313             @NonNull Set<String> allExistingStaticPackages,
314             @NonNull Set<String> removedFunctionPackages) {
315         ArraySet<String> removedPackages = new ArraySet<>();
316 
317         for (String packageName : removedFunctionPackages) {
318             if (!allExistingStaticPackages.contains(packageName)) {
319                 removedPackages.add(packageName);
320             }
321         }
322 
323         return removedPackages;
324     }
325 
326     /**
327      * This method returns a map of package names to a set of function ids that are in the static
328      * metadata but not in the runtime metadata.
329      *
330      * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
331      *     static metadata.
332      * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
333      *     runtime metadata.
334      * @return A map of package names to a set of function ids that are in the static metadata but
335      *     not in the runtime metadata.
336      */
337     @NonNull
338     @VisibleForTesting
getAddedFunctionsDiffMap( @onNull ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, @NonNull ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap)339     static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap(
340             @NonNull ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
341             @NonNull ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
342         Objects.requireNonNull(staticPackageToFunctionMap);
343         Objects.requireNonNull(runtimePackageToFunctionMap);
344 
345         return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
346     }
347 
348     /**
349      * This method returns a map of package names to a set of function ids that are in the runtime
350      * metadata but not in the static metadata.
351      *
352      * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
353      *     static metadata.
354      * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
355      *     runtime metadata.
356      * @return A map of package names to a set of function ids that are in the runtime metadata but
357      *     not in the static metadata.
358      */
359     @NonNull
360     @VisibleForTesting
getRemovedFunctionsDiffMap( @onNull ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, @NonNull ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap)361     static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap(
362             @NonNull ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
363             @NonNull ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
364         Objects.requireNonNull(staticPackageToFunctionMap);
365         Objects.requireNonNull(runtimePackageToFunctionMap);
366 
367         return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap);
368     }
369 
370     @NonNull
getFunctionsDiffMap( @onNull ArrayMap<String, ArraySet<String>> packageToFunctionMapA, @NonNull ArrayMap<String, ArraySet<String>> packageToFunctionMapB)371     private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap(
372             @NonNull ArrayMap<String, ArraySet<String>> packageToFunctionMapA,
373             @NonNull ArrayMap<String, ArraySet<String>> packageToFunctionMapB) {
374         Objects.requireNonNull(packageToFunctionMapA);
375         Objects.requireNonNull(packageToFunctionMapB);
376 
377         ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>();
378         for (String packageName : packageToFunctionMapA.keySet()) {
379             if (!packageToFunctionMapB.containsKey(packageName)) {
380                 diffMap.put(packageName, packageToFunctionMapA.get(packageName));
381                 continue;
382             }
383             ArraySet<String> diffFunctions = new ArraySet<>();
384             for (String functionId :
385                     Objects.requireNonNull(packageToFunctionMapA.get(packageName))) {
386                 if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName))
387                         .contains(functionId)) {
388                     diffFunctions.add(functionId);
389                 }
390             }
391             if (!diffFunctions.isEmpty()) {
392                 diffMap.put(packageName, diffFunctions);
393             }
394         }
395         return diffMap;
396     }
397 
398     /**
399      * This method returns a map of package names to a set of function ids from the AppFunction
400      * metadata.
401      *
402      * @param searchSession The {@link FutureAppSearchSession} to search the AppFunction metadata.
403      * @param schemaType The schema type of the AppFunction metadata.
404      * @param propertyFunctionId The property name of the function id in the AppFunction metadata.
405      * @param propertyPackageName The property name of the package name in the AppFunction metadata.
406      * @return A map of package names to a set of function ids from the AppFunction metadata.
407      */
408     @NonNull
409     @VisibleForTesting
410     @WorkerThread
getPackageToFunctionIdMap( @onNull FutureAppSearchSession searchSession, @NonNull String schemaType, @NonNull String propertyFunctionId, @NonNull String propertyPackageName)411     static ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap(
412             @NonNull FutureAppSearchSession searchSession,
413             @NonNull String schemaType,
414             @NonNull String propertyFunctionId,
415             @NonNull String propertyPackageName)
416             throws ExecutionException, InterruptedException {
417         Objects.requireNonNull(schemaType);
418         Objects.requireNonNull(propertyFunctionId);
419         Objects.requireNonNull(propertyPackageName);
420         ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>();
421 
422         try (FutureSearchResults futureSearchResults =
423                 searchSession
424                         .search(
425                                 "",
426                                 buildMetadataSearchSpec(
427                                         schemaType, propertyFunctionId, propertyPackageName))
428                         .get(); ) {
429             List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get();
430             // TODO(b/357551503): This could be expensive if we have more functions
431             while (!searchResultsList.isEmpty()) {
432                 for (SearchResult searchResult : searchResultsList) {
433                     String packageName =
434                             searchResult
435                                     .getGenericDocument()
436                                     .getPropertyString(propertyPackageName);
437                     String functionId =
438                             searchResult.getGenericDocument().getPropertyString(propertyFunctionId);
439                     packageToFunctionIds
440                             .computeIfAbsent(packageName, k -> new ArraySet<>())
441                             .add(functionId);
442                 }
443                 searchResultsList = futureSearchResults.getNextPage().get();
444             }
445         }
446         return packageToFunctionIds;
447     }
448 
449     /**
450      * This method returns a {@link SearchSpec} for searching the AppFunction metadata.
451      *
452      * @param schemaType The schema type of the AppFunction metadata.
453      * @param propertyFunctionId The property name of the function id in the AppFunction metadata.
454      * @param propertyPackageName The property name of the package name in the AppFunction metadata.
455      * @return A {@link SearchSpec} for searching the AppFunction metadata.
456      */
457     @NonNull
buildMetadataSearchSpec( @onNull String schemaType, @NonNull String propertyFunctionId, @NonNull String propertyPackageName)458     private static SearchSpec buildMetadataSearchSpec(
459             @NonNull String schemaType,
460             @NonNull String propertyFunctionId,
461             @NonNull String propertyPackageName) {
462         Objects.requireNonNull(schemaType);
463         Objects.requireNonNull(propertyFunctionId);
464         Objects.requireNonNull(propertyPackageName);
465         return new SearchSpec.Builder()
466                 .addFilterSchemas(schemaType)
467                 .addProjectionPaths(
468                         schemaType,
469                         List.of(
470                                 new PropertyPath(propertyFunctionId),
471                                 new PropertyPath(propertyPackageName)))
472                 .build();
473     }
474 
475     /** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found. */
476     @Nullable
getCertificate( @onNull PackageManager packageManager, @NonNull String packageName)477     private byte[] getCertificate(
478             @NonNull PackageManager packageManager, @NonNull String packageName) {
479         Objects.requireNonNull(packageManager);
480         Objects.requireNonNull(packageName);
481         PackageInfo packageInfo;
482         try {
483             packageInfo =
484                     Objects.requireNonNull(
485                             packageManager.getPackageInfo(
486                                     packageName,
487                                     PackageManager.GET_META_DATA
488                                             | PackageManager.GET_SIGNING_CERTIFICATES));
489         } catch (Exception e) {
490             Slog.d(TAG, "Package name info not found for package: " + packageName);
491             return null;
492         }
493         if (packageInfo.signingInfo == null) {
494             Slog.d(TAG, "Signing info not found for package: " + packageInfo.packageName);
495             return null;
496         }
497 
498         MessageDigest md;
499         try {
500             md = MessageDigest.getInstance("SHA256");
501         } catch (NoSuchAlgorithmException e) {
502             return null;
503         }
504         Signature[] signatures = packageInfo.signingInfo.getSigningCertificateHistory();
505         if (signatures == null || signatures.length == 0) {
506             return null;
507         }
508         md.update(signatures[0].toByteArray());
509         return md.digest();
510     }
511 }
512