DexManager.java revision 1fade2fc6d10fa90b58c4b3718206afaeee0723d
1/* 2 * Copyright (C) 2016 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 17package com.android.server.pm.dex; 18 19import android.content.pm.ApplicationInfo; 20import android.content.pm.IPackageManager; 21import android.content.pm.PackageInfo; 22import android.os.FileUtils; 23import android.os.RemoteException; 24import android.os.storage.StorageManager; 25import android.os.UserHandle; 26 27import android.util.Slog; 28 29import com.android.internal.annotations.GuardedBy; 30import com.android.server.pm.Installer; 31import com.android.server.pm.Installer.InstallerException; 32import com.android.server.pm.PackageDexOptimizer; 33import com.android.server.pm.PackageManagerServiceUtils; 34import com.android.server.pm.PackageManagerServiceCompilerMapping; 35 36import java.io.File; 37import java.io.IOException; 38import java.util.List; 39import java.util.HashMap; 40import java.util.HashSet; 41import java.util.Map; 42import java.util.Set; 43 44import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo; 45import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo; 46 47/** 48 * This class keeps track of how dex files are used. 49 * Every time it gets a notification about a dex file being loaded it tracks 50 * its owning package and records it in PackageDexUsage (package-dex-usage.list). 51 * 52 * TODO(calin): Extract related dexopt functionality from PackageManagerService 53 * into this class. 54 */ 55public class DexManager { 56 private static final String TAG = "DexManager"; 57 58 private static final boolean DEBUG = false; 59 60 // Maps package name to code locations. 61 // It caches the code locations for the installed packages. This allows for 62 // faster lookups (no locks) when finding what package owns the dex file. 63 @GuardedBy("mPackageCodeLocationsCache") 64 private final Map<String, PackageCodeLocations> mPackageCodeLocationsCache; 65 66 // PackageDexUsage handles the actual I/O operations. It is responsible to 67 // encode and save the dex usage data. 68 private final PackageDexUsage mPackageDexUsage; 69 70 private final IPackageManager mPackageManager; 71 private final PackageDexOptimizer mPackageDexOptimizer; 72 private final Object mInstallLock; 73 @GuardedBy("mInstallLock") 74 private final Installer mInstaller; 75 76 // Possible outcomes of a dex search. 77 private static int DEX_SEARCH_NOT_FOUND = 0; // dex file not found 78 private static int DEX_SEARCH_FOUND_PRIMARY = 1; // dex file is the primary/base apk 79 private static int DEX_SEARCH_FOUND_SPLIT = 2; // dex file is a split apk 80 private static int DEX_SEARCH_FOUND_SECONDARY = 3; // dex file is a secondary dex 81 82 public DexManager(IPackageManager pms, PackageDexOptimizer pdo, 83 Installer installer, Object installLock) { 84 mPackageCodeLocationsCache = new HashMap<>(); 85 mPackageDexUsage = new PackageDexUsage(); 86 mPackageManager = pms; 87 mPackageDexOptimizer = pdo; 88 mInstaller = installer; 89 mInstallLock = installLock; 90 } 91 92 /** 93 * Notify about dex files loads. 94 * Note that this method is invoked when apps load dex files and it should 95 * return as fast as possible. 96 * 97 * @param loadingAppInfo the package performing the load 98 * @param dexPaths the list of dex files being loaded 99 * @param loaderIsa the ISA of the app loading the dex files 100 * @param loaderUserId the user id which runs the code loading the dex files 101 */ 102 public void notifyDexLoad(ApplicationInfo loadingAppInfo, List<String> dexPaths, 103 String loaderIsa, int loaderUserId) { 104 try { 105 notifyDexLoadInternal(loadingAppInfo, dexPaths, loaderIsa, loaderUserId); 106 } catch (Exception e) { 107 Slog.w(TAG, "Exception while notifying dex load for package " + 108 loadingAppInfo.packageName, e); 109 } 110 } 111 112 private void notifyDexLoadInternal(ApplicationInfo loadingAppInfo, List<String> dexPaths, 113 String loaderIsa, int loaderUserId) { 114 if (!PackageManagerServiceUtils.checkISA(loaderIsa)) { 115 Slog.w(TAG, "Loading dex files " + dexPaths + " in unsupported ISA: " + 116 loaderIsa + "?"); 117 return; 118 } 119 120 for (String dexPath : dexPaths) { 121 // Find the owning package name. 122 DexSearchResult searchResult = getDexPackage(loadingAppInfo, dexPath, loaderUserId); 123 124 if (DEBUG) { 125 Slog.i(TAG, loadingAppInfo.packageName 126 + " loads from " + searchResult + " : " + loaderUserId + " : " + dexPath); 127 } 128 129 if (searchResult.mOutcome != DEX_SEARCH_NOT_FOUND) { 130 // TODO(calin): extend isUsedByOtherApps check to detect the cases where 131 // different apps share the same runtime. In that case we should not mark the dex 132 // file as isUsedByOtherApps. Currently this is a safe approximation. 133 boolean isUsedByOtherApps = !loadingAppInfo.packageName.equals( 134 searchResult.mOwningPackageName); 135 boolean primaryOrSplit = searchResult.mOutcome == DEX_SEARCH_FOUND_PRIMARY || 136 searchResult.mOutcome == DEX_SEARCH_FOUND_SPLIT; 137 138 if (primaryOrSplit && !isUsedByOtherApps) { 139 // If the dex file is the primary apk (or a split) and not isUsedByOtherApps 140 // do not record it. This case does not bring any new usable information 141 // and can be safely skipped. 142 continue; 143 } 144 145 // Record dex file usage. If the current usage is a new pattern (e.g. new secondary, 146 // or UsedBytOtherApps), record will return true and we trigger an async write 147 // to disk to make sure we don't loose the data in case of a reboot. 148 if (mPackageDexUsage.record(searchResult.mOwningPackageName, 149 dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit)) { 150 mPackageDexUsage.maybeWriteAsync(); 151 } 152 } else { 153 // This can happen in a few situations: 154 // - bogus dex loads 155 // - recent installs/uninstalls that we didn't detect. 156 // - new installed splits 157 // If we can't find the owner of the dex we simply do not track it. The impact is 158 // that the dex file will not be considered for offline optimizations. 159 // TODO(calin): add hooks for move/uninstall notifications to 160 // capture package moves or obsolete packages. 161 if (DEBUG) { 162 Slog.i(TAG, "Could not find owning package for dex file: " + dexPath); 163 } 164 } 165 } 166 } 167 168 /** 169 * Read the dex usage from disk and populate the code cache locations. 170 * @param existingPackages a map containing information about what packages 171 * are available to what users. Only packages in this list will be 172 * recognized during notifyDexLoad(). 173 */ 174 public void load(Map<Integer, List<PackageInfo>> existingPackages) { 175 try { 176 loadInternal(existingPackages); 177 } catch (Exception e) { 178 mPackageDexUsage.clear(); 179 Slog.w(TAG, "Exception while loading package dex usage. " + 180 "Starting with a fresh state.", e); 181 } 182 } 183 184 /** 185 * Notifies that a new package was installed for {@code userId}. 186 * {@code userId} must not be {@code UserHandle.USER_ALL}. 187 * 188 * @throws IllegalArgumentException if {@code userId} is {@code UserHandle.USER_ALL}. 189 */ 190 public void notifyPackageInstalled(PackageInfo pi, int userId) { 191 if (userId == UserHandle.USER_ALL) { 192 throw new IllegalArgumentException( 193 "notifyPackageInstalled called with USER_ALL"); 194 } 195 cachePackageInfo(pi, userId); 196 } 197 198 /** 199 * Notifies that package {@code packageName} was updated. 200 * This will clear the UsedByOtherApps mark if it exists. 201 */ 202 public void notifyPackageUpdated(String packageName, String baseCodePath, 203 String[] splitCodePaths) { 204 cachePackageCodeLocation(packageName, baseCodePath, splitCodePaths, null, /*userId*/ -1); 205 // In case there was an update, write the package use info to disk async. 206 // Note that we do the writing here and not in PackageDexUsage in order to be 207 // consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs 208 // multiple updates in PackageDexUsage before writing it). 209 if (mPackageDexUsage.clearUsedByOtherApps(packageName)) { 210 mPackageDexUsage.maybeWriteAsync(); 211 } 212 } 213 214 /** 215 * Notifies that the user {@code userId} data for package {@code packageName} 216 * was destroyed. This will remove all usage info associated with the package 217 * for the given user. 218 * {@code userId} is allowed to be {@code UserHandle.USER_ALL} in which case 219 * all usage information for the package will be removed. 220 */ 221 public void notifyPackageDataDestroyed(String packageName, int userId) { 222 boolean updated = userId == UserHandle.USER_ALL 223 ? mPackageDexUsage.removePackage(packageName) 224 : mPackageDexUsage.removeUserPackage(packageName, userId); 225 // In case there was an update, write the package use info to disk async. 226 // Note that we do the writing here and not in PackageDexUsage in order to be 227 // consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs 228 // multiple updates in PackageDexUsage before writing it). 229 if (updated) { 230 mPackageDexUsage.maybeWriteAsync(); 231 } 232 } 233 234 /** 235 * Caches the code location from the given package info. 236 */ 237 private void cachePackageInfo(PackageInfo pi, int userId) { 238 ApplicationInfo ai = pi.applicationInfo; 239 String[] dataDirs = new String[] {ai.dataDir, ai.deviceProtectedDataDir, 240 ai.credentialProtectedDataDir}; 241 cachePackageCodeLocation(pi.packageName, ai.sourceDir, ai.splitSourceDirs, 242 dataDirs, userId); 243 } 244 245 private void cachePackageCodeLocation(String packageName, String baseCodePath, 246 String[] splitCodePaths, String[] dataDirs, int userId) { 247 synchronized (mPackageCodeLocationsCache) { 248 PackageCodeLocations pcl = putIfAbsent(mPackageCodeLocationsCache, packageName, 249 new PackageCodeLocations(packageName, baseCodePath, splitCodePaths)); 250 // TODO(calin): We are forced to extend the scope of this synchronization because 251 // the values of the cache (PackageCodeLocations) are updated in place. 252 // Make PackageCodeLocations immutable to simplify the synchronization reasoning. 253 pcl.updateCodeLocation(baseCodePath, splitCodePaths); 254 if (dataDirs != null) { 255 for (String dataDir : dataDirs) { 256 // The set of data dirs includes deviceProtectedDataDir and 257 // credentialProtectedDataDir which might be null for shared 258 // libraries. Currently we don't track these but be lenient 259 // and check in case we ever decide to store their usage data. 260 if (dataDir != null) { 261 pcl.mergeAppDataDirs(dataDir, userId); 262 } 263 } 264 } 265 } 266 } 267 268 private void loadInternal(Map<Integer, List<PackageInfo>> existingPackages) { 269 Map<String, Set<Integer>> packageToUsersMap = new HashMap<>(); 270 // Cache the code locations for the installed packages. This allows for 271 // faster lookups (no locks) when finding what package owns the dex file. 272 for (Map.Entry<Integer, List<PackageInfo>> entry : existingPackages.entrySet()) { 273 List<PackageInfo> packageInfoList = entry.getValue(); 274 int userId = entry.getKey(); 275 for (PackageInfo pi : packageInfoList) { 276 // Cache the code locations. 277 cachePackageInfo(pi, userId); 278 279 // Cache a map from package name to the set of user ids who installed the package. 280 // We will use it to sync the data and remove obsolete entries from 281 // mPackageDexUsage. 282 Set<Integer> users = putIfAbsent( 283 packageToUsersMap, pi.packageName, new HashSet<>()); 284 users.add(userId); 285 } 286 } 287 288 mPackageDexUsage.read(); 289 mPackageDexUsage.syncData(packageToUsersMap); 290 } 291 292 /** 293 * Get the package dex usage for the given package name. 294 * @return the package data or null if there is no data available for this package. 295 */ 296 public PackageUseInfo getPackageUseInfo(String packageName) { 297 return mPackageDexUsage.getPackageUseInfo(packageName); 298 } 299 300 /** 301 * Perform dexopt on the package {@code packageName} secondary dex files. 302 * @return true if all secondary dex files were processed successfully (compiled or skipped 303 * because they don't need to be compiled).. 304 */ 305 public boolean dexoptSecondaryDex(String packageName, int compilerReason, boolean force) { 306 return dexoptSecondaryDex(packageName, 307 PackageManagerServiceCompilerMapping.getCompilerFilterForReason(compilerReason), 308 force); 309 } 310 311 /** 312 * Perform dexopt on the package {@code packageName} secondary dex files. 313 * @return true if all secondary dex files were processed successfully (compiled or skipped 314 * because they don't need to be compiled).. 315 */ 316 public boolean dexoptSecondaryDex(String packageName, String compilerFilter, boolean force) { 317 // Select the dex optimizer based on the force parameter. 318 // Forced compilation is done through ForcedUpdatePackageDexOptimizer which will adjust 319 // the necessary dexopt flags to make sure that compilation is not skipped. This avoid 320 // passing the force flag through the multitude of layers. 321 // Note: The force option is rarely used (cmdline input for testing, mostly), so it's OK to 322 // allocate an object here. 323 PackageDexOptimizer pdo = force 324 ? new PackageDexOptimizer.ForcedUpdatePackageDexOptimizer(mPackageDexOptimizer) 325 : mPackageDexOptimizer; 326 PackageUseInfo useInfo = getPackageUseInfo(packageName); 327 if (useInfo == null || useInfo.getDexUseInfoMap().isEmpty()) { 328 if (DEBUG) { 329 Slog.d(TAG, "No secondary dex use for package:" + packageName); 330 } 331 // Nothing to compile, return true. 332 return true; 333 } 334 boolean success = true; 335 for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) { 336 String dexPath = entry.getKey(); 337 DexUseInfo dexUseInfo = entry.getValue(); 338 PackageInfo pkg = null; 339 try { 340 pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0, 341 dexUseInfo.getOwnerUserId()); 342 } catch (RemoteException e) { 343 throw new AssertionError(e); 344 } 345 // It may be that the package gets uninstalled while we try to compile its 346 // secondary dex files. If that's the case, just ignore. 347 // Note that we don't break the entire loop because the package might still be 348 // installed for other users. 349 if (pkg == null) { 350 Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName 351 + " for user " + dexUseInfo.getOwnerUserId()); 352 mPackageDexUsage.removeUserPackage(packageName, dexUseInfo.getOwnerUserId()); 353 continue; 354 } 355 356 int result = pdo.dexOptSecondaryDexPath(pkg.applicationInfo, dexPath, 357 dexUseInfo.getLoaderIsas(), compilerFilter, dexUseInfo.isUsedByOtherApps()); 358 success = success && (result != PackageDexOptimizer.DEX_OPT_FAILED); 359 } 360 return success; 361 } 362 363 /** 364 * Reconcile the information we have about the secondary dex files belonging to 365 * {@code packagName} and the actual dex files. For all dex files that were 366 * deleted, update the internal records and delete any generated oat files. 367 */ 368 public void reconcileSecondaryDexFiles(String packageName) { 369 PackageUseInfo useInfo = getPackageUseInfo(packageName); 370 if (useInfo == null || useInfo.getDexUseInfoMap().isEmpty()) { 371 if (DEBUG) { 372 Slog.d(TAG, "No secondary dex use for package:" + packageName); 373 } 374 // Nothing to reconcile. 375 return; 376 } 377 378 boolean updated = false; 379 for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) { 380 String dexPath = entry.getKey(); 381 DexUseInfo dexUseInfo = entry.getValue(); 382 PackageInfo pkg = null; 383 try { 384 // Note that we look for the package in the PackageManager just to be able 385 // to get back the real app uid and its storage kind. These are only used 386 // to perform extra validation in installd. 387 // TODO(calin): maybe a bit overkill. 388 pkg = mPackageManager.getPackageInfo(packageName, /*flags*/0, 389 dexUseInfo.getOwnerUserId()); 390 } catch (RemoteException ignore) { 391 // Can't happen, DexManager is local. 392 } 393 if (pkg == null) { 394 // It may be that the package was uninstalled while we process the secondary 395 // dex files. 396 Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName 397 + " for user " + dexUseInfo.getOwnerUserId()); 398 // Update the usage and continue, another user might still have the package. 399 updated = mPackageDexUsage.removeUserPackage( 400 packageName, dexUseInfo.getOwnerUserId()) || updated; 401 continue; 402 } 403 ApplicationInfo info = pkg.applicationInfo; 404 int flags = 0; 405 if (info.deviceProtectedDataDir != null && 406 FileUtils.contains(info.deviceProtectedDataDir, dexPath)) { 407 flags |= StorageManager.FLAG_STORAGE_DE; 408 } else if (info.credentialProtectedDataDir!= null && 409 FileUtils.contains(info.credentialProtectedDataDir, dexPath)) { 410 flags |= StorageManager.FLAG_STORAGE_CE; 411 } else { 412 Slog.e(TAG, "Could not infer CE/DE storage for path " + dexPath); 413 updated = mPackageDexUsage.removeDexFile( 414 packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated; 415 continue; 416 } 417 418 boolean dexStillExists = true; 419 synchronized(mInstallLock) { 420 try { 421 String[] isas = dexUseInfo.getLoaderIsas().toArray(new String[0]); 422 dexStillExists = mInstaller.reconcileSecondaryDexFile(dexPath, packageName, 423 pkg.applicationInfo.uid, isas, pkg.applicationInfo.volumeUuid, flags); 424 } catch (InstallerException e) { 425 Slog.e(TAG, "Got InstallerException when reconciling dex " + dexPath + 426 " : " + e.getMessage()); 427 } 428 } 429 if (!dexStillExists) { 430 updated = mPackageDexUsage.removeDexFile( 431 packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated; 432 } 433 434 } 435 if (updated) { 436 mPackageDexUsage.maybeWriteAsync(); 437 } 438 } 439 440 /** 441 * Return all packages that contain records of secondary dex files. 442 */ 443 public Set<String> getAllPackagesWithSecondaryDexFiles() { 444 return mPackageDexUsage.getAllPackagesWithSecondaryDexFiles(); 445 } 446 447 /** 448 * Return true if the profiling data collected for the given app indicate 449 * that the apps's APK has been loaded by another app. 450 * Note that this returns false for all apps without any collected profiling data. 451 */ 452 public boolean isUsedByOtherApps(String packageName) { 453 PackageUseInfo useInfo = getPackageUseInfo(packageName); 454 if (useInfo == null) { 455 // No use info, means the package was not used or it was used but not by other apps. 456 // Note that right now we might prune packages which are not used by other apps. 457 // TODO(calin): maybe we should not (prune) so we can have an accurate view when we try 458 // to access the package use. 459 return false; 460 } 461 return useInfo.isUsedByOtherApps(); 462 } 463 464 /** 465 * Retrieves the package which owns the given dexPath. 466 */ 467 private DexSearchResult getDexPackage( 468 ApplicationInfo loadingAppInfo, String dexPath, int userId) { 469 // Ignore framework code. 470 // TODO(calin): is there a better way to detect it? 471 if (dexPath.startsWith("/system/framework/")) { 472 return new DexSearchResult("framework", DEX_SEARCH_NOT_FOUND); 473 } 474 475 // First, check if the package which loads the dex file actually owns it. 476 // Most of the time this will be true and we can return early. 477 PackageCodeLocations loadingPackageCodeLocations = 478 new PackageCodeLocations(loadingAppInfo, userId); 479 int outcome = loadingPackageCodeLocations.searchDex(dexPath, userId); 480 if (outcome != DEX_SEARCH_NOT_FOUND) { 481 // TODO(calin): evaluate if we bother to detect symlinks at the dexPath level. 482 return new DexSearchResult(loadingPackageCodeLocations.mPackageName, outcome); 483 } 484 485 // The loadingPackage does not own the dex file. 486 // Perform a reverse look-up in the cache to detect if any package has ownership. 487 // Note that we can have false negatives if the cache falls out of date. 488 synchronized (mPackageCodeLocationsCache) { 489 for (PackageCodeLocations pcl : mPackageCodeLocationsCache.values()) { 490 outcome = pcl.searchDex(dexPath, userId); 491 if (outcome != DEX_SEARCH_NOT_FOUND) { 492 return new DexSearchResult(pcl.mPackageName, outcome); 493 } 494 } 495 } 496 497 if (DEBUG) { 498 // TODO(calin): Consider checking for /data/data symlink. 499 // /data/data/ symlinks /data/user/0/ and there's nothing stopping apps 500 // to load dex files through it. 501 try { 502 String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath)); 503 if (dexPathReal != dexPath) { 504 Slog.d(TAG, "Dex loaded with symlink. dexPath=" + 505 dexPath + " dexPathReal=" + dexPathReal); 506 } 507 } catch (IOException e) { 508 // Ignore 509 } 510 } 511 // Cache miss. The cache is updated during installs and uninstalls, 512 // so if we get here we're pretty sure the dex path does not exist. 513 return new DexSearchResult(null, DEX_SEARCH_NOT_FOUND); 514 } 515 516 private static <K,V> V putIfAbsent(Map<K,V> map, K key, V newValue) { 517 V existingValue = map.putIfAbsent(key, newValue); 518 return existingValue == null ? newValue : existingValue; 519 } 520 521 /** 522 * Convenience class to store the different locations where a package might 523 * own code. 524 */ 525 private static class PackageCodeLocations { 526 private final String mPackageName; 527 private String mBaseCodePath; 528 private final Set<String> mSplitCodePaths; 529 // Maps user id to the application private directory. 530 private final Map<Integer, Set<String>> mAppDataDirs; 531 532 public PackageCodeLocations(ApplicationInfo ai, int userId) { 533 this(ai.packageName, ai.sourceDir, ai.splitSourceDirs); 534 mergeAppDataDirs(ai.dataDir, userId); 535 } 536 public PackageCodeLocations(String packageName, String baseCodePath, 537 String[] splitCodePaths) { 538 mPackageName = packageName; 539 mSplitCodePaths = new HashSet<>(); 540 mAppDataDirs = new HashMap<>(); 541 updateCodeLocation(baseCodePath, splitCodePaths); 542 } 543 544 public void updateCodeLocation(String baseCodePath, String[] splitCodePaths) { 545 mBaseCodePath = baseCodePath; 546 mSplitCodePaths.clear(); 547 if (splitCodePaths != null) { 548 for (String split : splitCodePaths) { 549 mSplitCodePaths.add(split); 550 } 551 } 552 } 553 554 public void mergeAppDataDirs(String dataDir, int userId) { 555 Set<String> dataDirs = putIfAbsent(mAppDataDirs, userId, new HashSet<>()); 556 dataDirs.add(dataDir); 557 } 558 559 public int searchDex(String dexPath, int userId) { 560 // First check that this package is installed or active for the given user. 561 // A missing data dir means the package is not installed. 562 Set<String> userDataDirs = mAppDataDirs.get(userId); 563 if (userDataDirs == null) { 564 return DEX_SEARCH_NOT_FOUND; 565 } 566 567 if (mBaseCodePath.equals(dexPath)) { 568 return DEX_SEARCH_FOUND_PRIMARY; 569 } 570 if (mSplitCodePaths.contains(dexPath)) { 571 return DEX_SEARCH_FOUND_SPLIT; 572 } 573 for (String dataDir : userDataDirs) { 574 if (dexPath.startsWith(dataDir)) { 575 return DEX_SEARCH_FOUND_SECONDARY; 576 } 577 } 578 579 return DEX_SEARCH_NOT_FOUND; 580 } 581 } 582 583 /** 584 * Convenience class to store ownership search results. 585 */ 586 private class DexSearchResult { 587 private String mOwningPackageName; 588 private int mOutcome; 589 590 public DexSearchResult(String owningPackageName, int outcome) { 591 this.mOwningPackageName = owningPackageName; 592 this.mOutcome = outcome; 593 } 594 595 @Override 596 public String toString() { 597 return mOwningPackageName + "-" + mOutcome; 598 } 599 } 600 601 602} 603