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