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