PackageManagerServiceUtils.java revision 420d58a9d867a3ce96fb4ea98bd30ee4d44eab4d
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;
18
19import static android.content.pm.PackageManager.INSTALL_FAILED_SHARED_USER_INCOMPATIBLE;
20import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE;
21import static com.android.server.pm.PackageManagerService.COMPRESSED_EXTENSION;
22import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION;
23import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
24import static com.android.server.pm.PackageManagerService.STUB_SUFFIX;
25import static com.android.server.pm.PackageManagerService.TAG;
26import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
27
28import com.android.internal.content.NativeLibraryHelper;
29import com.android.internal.util.ArrayUtils;
30import com.android.internal.util.FastPrintWriter;
31import com.android.server.EventLogTags;
32import com.android.server.pm.dex.DexManager;
33import com.android.server.pm.dex.PackageDexUsage;
34
35import android.annotation.NonNull;
36import android.app.AppGlobals;
37import android.content.Intent;
38import android.content.pm.PackageManager;
39import android.content.pm.PackageParser;
40import android.content.pm.ResolveInfo;
41import android.content.pm.Signature;
42import android.os.Build;
43import android.os.Debug;
44import android.os.Environment;
45import android.os.FileUtils;
46import android.os.Process;
47import android.os.RemoteException;
48import android.os.UserHandle;
49import android.service.pm.PackageServiceDumpProto;
50import android.system.ErrnoException;
51import android.system.Os;
52import android.util.ArraySet;
53import android.util.Log;
54import android.util.Slog;
55import android.util.jar.StrictJarFile;
56import android.util.proto.ProtoOutputStream;
57
58import dalvik.system.VMRuntime;
59
60import libcore.io.IoUtils;
61import libcore.io.Libcore;
62import libcore.io.Streams;
63
64import java.io.BufferedReader;
65import java.io.File;
66import java.io.FileInputStream;
67import java.io.FileOutputStream;
68import java.io.FileReader;
69import java.io.FilenameFilter;
70import java.io.IOException;
71import java.io.InputStream;
72import java.io.OutputStream;
73import java.io.PrintWriter;
74import java.security.cert.CertificateEncodingException;
75import java.security.cert.CertificateException;
76import java.text.SimpleDateFormat;
77import java.util.ArrayList;
78import java.util.Arrays;
79import java.util.Collection;
80import java.util.Collections;
81import java.util.Date;
82import java.util.Iterator;
83import java.util.LinkedList;
84import java.util.List;
85import java.util.function.Predicate;
86import java.util.zip.GZIPInputStream;
87import java.util.zip.ZipEntry;
88
89/**
90 * Class containing helper methods for the PackageManagerService.
91 *
92 * {@hide}
93 */
94public class PackageManagerServiceUtils {
95    private final static long SEVEN_DAYS_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
96
97    private static ArraySet<String> getPackageNamesForIntent(Intent intent, int userId) {
98        List<ResolveInfo> ris = null;
99        try {
100            ris = AppGlobals.getPackageManager().queryIntentReceivers(intent, null, 0, userId)
101                    .getList();
102        } catch (RemoteException e) {
103        }
104        ArraySet<String> pkgNames = new ArraySet<String>();
105        if (ris != null) {
106            for (ResolveInfo ri : ris) {
107                pkgNames.add(ri.activityInfo.packageName);
108            }
109        }
110        return pkgNames;
111    }
112
113    // Sort a list of apps by their last usage, most recently used apps first. The order of
114    // packages without usage data is undefined (but they will be sorted after the packages
115    // that do have usage data).
116    public static void sortPackagesByUsageDate(List<PackageParser.Package> pkgs,
117            PackageManagerService packageManagerService) {
118        if (!packageManagerService.isHistoricalPackageUsageAvailable()) {
119            return;
120        }
121
122        Collections.sort(pkgs, (pkg1, pkg2) ->
123                Long.compare(pkg2.getLatestForegroundPackageUseTimeInMills(),
124                        pkg1.getLatestForegroundPackageUseTimeInMills()));
125    }
126
127    // Apply the given {@code filter} to all packages in {@code packages}. If tested positive, the
128    // package will be removed from {@code packages} and added to {@code result} with its
129    // dependencies. If usage data is available, the positive packages will be sorted by usage
130    // data (with {@code sortTemp} as temporary storage).
131    private static void applyPackageFilter(Predicate<PackageParser.Package> filter,
132            Collection<PackageParser.Package> result,
133            Collection<PackageParser.Package> packages,
134            @NonNull List<PackageParser.Package> sortTemp,
135            PackageManagerService packageManagerService) {
136        for (PackageParser.Package pkg : packages) {
137            if (filter.test(pkg)) {
138                sortTemp.add(pkg);
139            }
140        }
141
142        sortPackagesByUsageDate(sortTemp, packageManagerService);
143        packages.removeAll(sortTemp);
144
145        for (PackageParser.Package pkg : sortTemp) {
146            result.add(pkg);
147
148            Collection<PackageParser.Package> deps =
149                    packageManagerService.findSharedNonSystemLibraries(pkg);
150            if (!deps.isEmpty()) {
151                deps.removeAll(result);
152                result.addAll(deps);
153                packages.removeAll(deps);
154            }
155        }
156
157        sortTemp.clear();
158    }
159
160    // Sort apps by importance for dexopt ordering. Important apps are given
161    // more priority in case the device runs out of space.
162    public static List<PackageParser.Package> getPackagesForDexopt(
163            Collection<PackageParser.Package> packages,
164            PackageManagerService packageManagerService) {
165        ArrayList<PackageParser.Package> remainingPkgs = new ArrayList<>(packages);
166        LinkedList<PackageParser.Package> result = new LinkedList<>();
167        ArrayList<PackageParser.Package> sortTemp = new ArrayList<>(remainingPkgs.size());
168
169        // Give priority to core apps.
170        applyPackageFilter((pkg) -> pkg.coreApp, result, remainingPkgs, sortTemp,
171                packageManagerService);
172
173        // Give priority to system apps that listen for pre boot complete.
174        Intent intent = new Intent(Intent.ACTION_PRE_BOOT_COMPLETED);
175        final ArraySet<String> pkgNames = getPackageNamesForIntent(intent, UserHandle.USER_SYSTEM);
176        applyPackageFilter((pkg) -> pkgNames.contains(pkg.packageName), result, remainingPkgs,
177                sortTemp, packageManagerService);
178
179        // Give priority to apps used by other apps.
180        DexManager dexManager = packageManagerService.getDexManager();
181        applyPackageFilter((pkg) ->
182                dexManager.getPackageUseInfoOrDefault(pkg.packageName)
183                        .isAnyCodePathUsedByOtherApps(),
184                result, remainingPkgs, sortTemp, packageManagerService);
185
186        // Filter out packages that aren't recently used, add all remaining apps.
187        // TODO: add a property to control this?
188        Predicate<PackageParser.Package> remainingPredicate;
189        if (!remainingPkgs.isEmpty() && packageManagerService.isHistoricalPackageUsageAvailable()) {
190            if (DEBUG_DEXOPT) {
191                Log.i(TAG, "Looking at historical package use");
192            }
193            // Get the package that was used last.
194            PackageParser.Package lastUsed = Collections.max(remainingPkgs, (pkg1, pkg2) ->
195                    Long.compare(pkg1.getLatestForegroundPackageUseTimeInMills(),
196                            pkg2.getLatestForegroundPackageUseTimeInMills()));
197            if (DEBUG_DEXOPT) {
198                Log.i(TAG, "Taking package " + lastUsed.packageName + " as reference in time use");
199            }
200            long estimatedPreviousSystemUseTime =
201                    lastUsed.getLatestForegroundPackageUseTimeInMills();
202            // Be defensive if for some reason package usage has bogus data.
203            if (estimatedPreviousSystemUseTime != 0) {
204                final long cutoffTime = estimatedPreviousSystemUseTime - SEVEN_DAYS_IN_MILLISECONDS;
205                remainingPredicate =
206                        (pkg) -> pkg.getLatestForegroundPackageUseTimeInMills() >= cutoffTime;
207            } else {
208                // No meaningful historical info. Take all.
209                remainingPredicate = (pkg) -> true;
210            }
211            sortPackagesByUsageDate(remainingPkgs, packageManagerService);
212        } else {
213            // No historical info. Take all.
214            remainingPredicate = (pkg) -> true;
215        }
216        applyPackageFilter(remainingPredicate, result, remainingPkgs, sortTemp,
217                packageManagerService);
218
219        if (DEBUG_DEXOPT) {
220            Log.i(TAG, "Packages to be dexopted: " + packagesToString(result));
221            Log.i(TAG, "Packages skipped from dexopt: " + packagesToString(remainingPkgs));
222        }
223
224        return result;
225    }
226
227    /**
228     * Checks if the package was inactive during since <code>thresholdTimeinMillis</code>.
229     * Package is considered active, if:
230     * 1) It was active in foreground.
231     * 2) It was active in background and also used by other apps.
232     *
233     * If it doesn't have sufficient information about the package, it return <code>false</code>.
234     */
235    public static boolean isUnusedSinceTimeInMillis(long firstInstallTime, long currentTimeInMillis,
236            long thresholdTimeinMillis, PackageDexUsage.PackageUseInfo packageUseInfo,
237            long latestPackageUseTimeInMillis, long latestForegroundPackageUseTimeInMillis) {
238
239        if (currentTimeInMillis - firstInstallTime < thresholdTimeinMillis) {
240            return false;
241        }
242
243        // If the app was active in foreground during the threshold period.
244        boolean isActiveInForeground = (currentTimeInMillis
245                - latestForegroundPackageUseTimeInMillis)
246                < thresholdTimeinMillis;
247
248        if (isActiveInForeground) {
249            return false;
250        }
251
252        // If the app was active in background during the threshold period and was used
253        // by other packages.
254        boolean isActiveInBackgroundAndUsedByOtherPackages = ((currentTimeInMillis
255                - latestPackageUseTimeInMillis)
256                < thresholdTimeinMillis)
257                && packageUseInfo.isAnyCodePathUsedByOtherApps();
258
259        return !isActiveInBackgroundAndUsedByOtherPackages;
260    }
261
262    /**
263     * Returns the canonicalized path of {@code path} as per {@code realpath(3)}
264     * semantics.
265     */
266    public static String realpath(File path) throws IOException {
267        try {
268            return Os.realpath(path.getAbsolutePath());
269        } catch (ErrnoException ee) {
270            throw ee.rethrowAsIOException();
271        }
272    }
273
274    public static String packagesToString(Collection<PackageParser.Package> c) {
275        StringBuilder sb = new StringBuilder();
276        for (PackageParser.Package pkg : c) {
277            if (sb.length() > 0) {
278                sb.append(", ");
279            }
280            sb.append(pkg.packageName);
281        }
282        return sb.toString();
283    }
284
285    /**
286     * Verifies that the given string {@code isa} is a valid supported isa on
287     * the running device.
288     */
289    public static boolean checkISA(String isa) {
290        for (String abi : Build.SUPPORTED_ABIS) {
291            if (VMRuntime.getInstructionSet(abi).equals(isa)) {
292                return true;
293            }
294        }
295        return false;
296    }
297
298    public static long getLastModifiedTime(PackageParser.Package pkg) {
299        final File srcFile = new File(pkg.codePath);
300        if (!srcFile.isDirectory()) {
301            return srcFile.lastModified();
302        }
303        final File baseFile = new File(pkg.baseCodePath);
304        long maxModifiedTime = baseFile.lastModified();
305        if (pkg.splitCodePaths != null) {
306            for (int i = pkg.splitCodePaths.length - 1; i >=0; --i) {
307                final File splitFile = new File(pkg.splitCodePaths[i]);
308                maxModifiedTime = Math.max(maxModifiedTime, splitFile.lastModified());
309            }
310        }
311        return maxModifiedTime;
312    }
313
314    /**
315     * Checks that the archive located at {@code fileName} has uncompressed dex file and so
316     * files that can be direclty mapped.
317     */
318    public static void logApkHasUncompressedCode(String fileName) {
319        StrictJarFile jarFile = null;
320        try {
321            jarFile = new StrictJarFile(fileName,
322                    false /*verify*/, false /*signatureSchemeRollbackProtectionsEnforced*/);
323            Iterator<ZipEntry> it = jarFile.iterator();
324            while (it.hasNext()) {
325                ZipEntry entry = it.next();
326                if (entry.getName().endsWith(".dex")) {
327                    if (entry.getMethod() != ZipEntry.STORED) {
328                        Slog.wtf(TAG, "APK " + fileName + " has compressed dex code " +
329                                entry.getName());
330                    } else if ((entry.getDataOffset() & 0x3) != 0) {
331                        Slog.wtf(TAG, "APK " + fileName + " has unaligned dex code " +
332                                entry.getName());
333                    }
334                } else if (entry.getName().endsWith(".so")) {
335                    if (entry.getMethod() != ZipEntry.STORED) {
336                        Slog.wtf(TAG, "APK " + fileName + " has compressed native code " +
337                                entry.getName());
338                    } else if ((entry.getDataOffset() & (0x1000 - 1)) != 0) {
339                        Slog.wtf(TAG, "APK " + fileName + " has unaligned native code " +
340                                entry.getName());
341                    }
342                }
343            }
344        } catch (IOException ignore) {
345            Slog.wtf(TAG, "Error when parsing APK " + fileName);
346        } finally {
347            try {
348                if (jarFile != null) {
349                    jarFile.close();
350                }
351            } catch (IOException ignore) {}
352        }
353        return;
354    }
355
356    /**
357     * Checks that the APKs in the given package have uncompressed dex file and so
358     * files that can be direclty mapped.
359     */
360    public static void logPackageHasUncompressedCode(PackageParser.Package pkg) {
361        logApkHasUncompressedCode(pkg.baseCodePath);
362        if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) {
363            for (int i = 0; i < pkg.splitCodePaths.length; i++) {
364                logApkHasUncompressedCode(pkg.splitCodePaths[i]);
365            }
366        }
367    }
368
369    private static File getSettingsProblemFile() {
370        File dataDir = Environment.getDataDirectory();
371        File systemDir = new File(dataDir, "system");
372        File fname = new File(systemDir, "uiderrors.txt");
373        return fname;
374    }
375
376    public static void dumpCriticalInfo(ProtoOutputStream proto) {
377        try (BufferedReader in = new BufferedReader(new FileReader(getSettingsProblemFile()))) {
378            String line = null;
379            while ((line = in.readLine()) != null) {
380                if (line.contains("ignored: updated version")) continue;
381                proto.write(PackageServiceDumpProto.MESSAGES, line);
382            }
383        } catch (IOException ignored) {
384        }
385    }
386
387    public static void dumpCriticalInfo(PrintWriter pw, String msg) {
388        try (BufferedReader in = new BufferedReader(new FileReader(getSettingsProblemFile()))) {
389            String line = null;
390            while ((line = in.readLine()) != null) {
391                if (line.contains("ignored: updated version")) continue;
392                if (msg != null) {
393                    pw.print(msg);
394                }
395                pw.println(line);
396            }
397        } catch (IOException ignored) {
398        }
399    }
400
401    public static void logCriticalInfo(int priority, String msg) {
402        Slog.println(priority, TAG, msg);
403        EventLogTags.writePmCriticalInfo(msg);
404        try {
405            File fname = getSettingsProblemFile();
406            FileOutputStream out = new FileOutputStream(fname, true);
407            PrintWriter pw = new FastPrintWriter(out);
408            SimpleDateFormat formatter = new SimpleDateFormat();
409            String dateString = formatter.format(new Date(System.currentTimeMillis()));
410            pw.println(dateString + ": " + msg);
411            pw.close();
412            FileUtils.setPermissions(
413                    fname.toString(),
414                    FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IROTH,
415                    -1, -1);
416        } catch (java.io.IOException e) {
417        }
418    }
419
420    public static void enforceShellRestriction(String restriction, int callingUid, int userHandle) {
421        if (callingUid == Process.SHELL_UID) {
422            if (userHandle >= 0
423                    && PackageManagerService.sUserManager.hasUserRestriction(
424                            restriction, userHandle)) {
425                throw new SecurityException("Shell does not have permission to access user "
426                        + userHandle);
427            } else if (userHandle < 0) {
428                Slog.e(PackageManagerService.TAG, "Unable to check shell permission for user "
429                        + userHandle + "\n\t" + Debug.getCallers(3));
430            }
431        }
432    }
433
434    /**
435     * Derive the value of the {@code cpuAbiOverride} based on the provided
436     * value and an optional stored value from the package settings.
437     */
438    public static String deriveAbiOverride(String abiOverride, PackageSetting settings) {
439        String cpuAbiOverride = null;
440        if (NativeLibraryHelper.CLEAR_ABI_OVERRIDE.equals(abiOverride)) {
441            cpuAbiOverride = null;
442        } else if (abiOverride != null) {
443            cpuAbiOverride = abiOverride;
444        } else if (settings != null) {
445            cpuAbiOverride = settings.cpuAbiOverrideString;
446        }
447        return cpuAbiOverride;
448    }
449
450    /**
451     * Compares two sets of signatures. Returns:
452     * <br />
453     * {@link PackageManager#SIGNATURE_NEITHER_SIGNED}: if both signature sets are null,
454     * <br />
455     * {@link PackageManager#SIGNATURE_FIRST_NOT_SIGNED}: if the first signature set is null,
456     * <br />
457     * {@link PackageManager#SIGNATURE_SECOND_NOT_SIGNED}: if the second signature set is null,
458     * <br />
459     * {@link PackageManager#SIGNATURE_MATCH}: if the two signature sets are identical,
460     * <br />
461     * {@link PackageManager#SIGNATURE_NO_MATCH}: if the two signature sets differ.
462     */
463    public static int compareSignatures(Signature[] s1, Signature[] s2) {
464        if (s1 == null) {
465            return s2 == null
466                    ? PackageManager.SIGNATURE_NEITHER_SIGNED
467                    : PackageManager.SIGNATURE_FIRST_NOT_SIGNED;
468        }
469
470        if (s2 == null) {
471            return PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
472        }
473
474        if (s1.length != s2.length) {
475            return PackageManager.SIGNATURE_NO_MATCH;
476        }
477
478        // Since both signature sets are of size 1, we can compare without HashSets.
479        if (s1.length == 1) {
480            return s1[0].equals(s2[0]) ?
481                    PackageManager.SIGNATURE_MATCH :
482                    PackageManager.SIGNATURE_NO_MATCH;
483        }
484
485        ArraySet<Signature> set1 = new ArraySet<Signature>();
486        for (Signature sig : s1) {
487            set1.add(sig);
488        }
489        ArraySet<Signature> set2 = new ArraySet<Signature>();
490        for (Signature sig : s2) {
491            set2.add(sig);
492        }
493        // Make sure s2 contains all signatures in s1.
494        if (set1.equals(set2)) {
495            return PackageManager.SIGNATURE_MATCH;
496        }
497        return PackageManager.SIGNATURE_NO_MATCH;
498    }
499
500    /**
501     * Used for backward compatibility to make sure any packages with
502     * certificate chains get upgraded to the new style. {@code existingSigs}
503     * will be in the old format (since they were stored on disk from before the
504     * system upgrade) and {@code scannedSigs} will be in the newer format.
505     */
506    private static boolean matchSignaturesCompat(String packageName,
507            PackageSignatures packageSignatures, PackageParser.SigningDetails parsedSignatures) {
508        ArraySet<Signature> existingSet = new ArraySet<Signature>();
509        for (Signature sig : packageSignatures.mSignatures) {
510            existingSet.add(sig);
511        }
512        ArraySet<Signature> scannedCompatSet = new ArraySet<Signature>();
513        for (Signature sig : parsedSignatures.signatures) {
514            try {
515                Signature[] chainSignatures = sig.getChainSignatures();
516                for (Signature chainSig : chainSignatures) {
517                    scannedCompatSet.add(chainSig);
518                }
519            } catch (CertificateEncodingException e) {
520                scannedCompatSet.add(sig);
521            }
522        }
523        // make sure the expanded scanned set contains all signatures in the existing one
524        if (scannedCompatSet.equals(existingSet)) {
525            // migrate the old signatures to the new scheme
526            packageSignatures.assignSignatures(parsedSignatures);
527            return true;
528        }
529        return false;
530    }
531
532    private static boolean matchSignaturesRecover(String packageName,
533            Signature[] existingSignatures, Signature[] parsedSignatures) {
534        String msg = null;
535        try {
536            if (Signature.areEffectiveMatch(existingSignatures, parsedSignatures)) {
537                logCriticalInfo(Log.INFO,
538                        "Recovered effectively matching certificates for " + packageName);
539                return true;
540            }
541        } catch (CertificateException e) {
542            msg = e.getMessage();
543        }
544        logCriticalInfo(Log.INFO,
545                "Failed to recover certificates for " + packageName + ": " + msg);
546        return false;
547    }
548
549    /**
550     * Verifies that signatures match.
551     * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}.
552     * @throws PackageManagerException if the signatures did not match.
553     */
554    public static boolean verifySignatures(PackageSetting pkgSetting,
555            PackageParser.SigningDetails parsedSignatures, boolean compareCompat,
556            boolean compareRecover)
557            throws PackageManagerException {
558        final String packageName = pkgSetting.name;
559        boolean compatMatch = false;
560        if (pkgSetting.signatures.mSignatures != null) {
561            // Already existing package. Make sure signatures match
562            boolean match = compareSignatures(pkgSetting.signatures.mSignatures,
563                    parsedSignatures.signatures)
564                    == PackageManager.SIGNATURE_MATCH;
565            if (!match && compareCompat) {
566                match = matchSignaturesCompat(packageName, pkgSetting.signatures,
567                        parsedSignatures);
568                compatMatch = match;
569            }
570            if (!match && compareRecover) {
571                match = matchSignaturesRecover(
572                        packageName, pkgSetting.signatures.mSignatures,
573                        parsedSignatures.signatures);
574            }
575            if (!match) {
576                throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
577                        "Package " + packageName +
578                        " signatures do not match previously installed version; ignoring!");
579            }
580        }
581        // Check for shared user signatures
582        if (pkgSetting.sharedUser != null && pkgSetting.sharedUser.signatures.mSignatures != null) {
583            // Already existing package. Make sure signatures match
584            boolean match = compareSignatures(pkgSetting.sharedUser.signatures.mSignatures,
585                    parsedSignatures.signatures) == PackageManager.SIGNATURE_MATCH;
586            if (!match && compareCompat) {
587                match = matchSignaturesCompat(
588                        packageName, pkgSetting.sharedUser.signatures, parsedSignatures);
589            }
590            if (!match && compareRecover) {
591                match = matchSignaturesRecover(packageName,
592                        pkgSetting.sharedUser.signatures.mSignatures, parsedSignatures.signatures);
593                compatMatch |= match;
594            }
595            if (!match) {
596                throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
597                        "Package " + packageName
598                        + " has no signatures that match those in shared user "
599                        + pkgSetting.sharedUser.name + "; ignoring!");
600            }
601        }
602        return compatMatch;
603    }
604
605    public static int decompressFile(File srcFile, File dstFile) throws ErrnoException {
606        if (DEBUG_COMPRESSION) {
607            Slog.i(TAG, "Decompress file"
608                    + "; src: " + srcFile.getAbsolutePath()
609                    + ", dst: " + dstFile.getAbsolutePath());
610        }
611        try (
612                InputStream fileIn = new GZIPInputStream(new FileInputStream(srcFile));
613                OutputStream fileOut = new FileOutputStream(dstFile, false /*append*/);
614        ) {
615            Streams.copy(fileIn, fileOut);
616            Os.chmod(dstFile.getAbsolutePath(), 0644);
617            return PackageManager.INSTALL_SUCCEEDED;
618        } catch (IOException e) {
619            logCriticalInfo(Log.ERROR, "Failed to decompress file"
620                    + "; src: " + srcFile.getAbsolutePath()
621                    + ", dst: " + dstFile.getAbsolutePath());
622        }
623        return PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
624    }
625
626    public static File[] getCompressedFiles(String codePath) {
627        final File stubCodePath = new File(codePath);
628        final String stubName = stubCodePath.getName();
629
630        // The layout of a compressed package on a given partition is as follows :
631        //
632        // Compressed artifacts:
633        //
634        // /partition/ModuleName/foo.gz
635        // /partation/ModuleName/bar.gz
636        //
637        // Stub artifact:
638        //
639        // /partition/ModuleName-Stub/ModuleName-Stub.apk
640        //
641        // In other words, stub is on the same partition as the compressed artifacts
642        // and in a directory that's suffixed with "-Stub".
643        int idx = stubName.lastIndexOf(STUB_SUFFIX);
644        if (idx < 0 || (stubName.length() != (idx + STUB_SUFFIX.length()))) {
645            return null;
646        }
647
648        final File stubParentDir = stubCodePath.getParentFile();
649        if (stubParentDir == null) {
650            Slog.e(TAG, "Unable to determine stub parent dir for codePath: " + codePath);
651            return null;
652        }
653
654        final File compressedPath = new File(stubParentDir, stubName.substring(0, idx));
655        final File[] files = compressedPath.listFiles(new FilenameFilter() {
656            @Override
657            public boolean accept(File dir, String name) {
658                return name.toLowerCase().endsWith(COMPRESSED_EXTENSION);
659            }
660        });
661
662        if (DEBUG_COMPRESSION && files != null && files.length > 0) {
663            Slog.i(TAG, "getCompressedFiles[" + codePath + "]: " + Arrays.toString(files));
664        }
665
666        return files;
667    }
668
669    public static boolean compressedFileExists(String codePath) {
670        final File[] compressedFiles = getCompressedFiles(codePath);
671        return compressedFiles != null && compressedFiles.length > 0;
672    }
673}
674