DefaultContainerService.java revision 6dceb88f1c7c42c6ab43834af2c993d599895d82
1/*
2 * Copyright (C) 2010 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.defcontainer;
18
19import com.android.internal.app.IMediaContainerService;
20import com.android.internal.content.NativeLibraryHelper;
21import com.android.internal.content.PackageHelper;
22
23import android.app.IntentService;
24import android.content.Intent;
25import android.content.pm.IPackageManager;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageInfoLite;
28import android.content.pm.PackageManager;
29import android.content.pm.PackageParser;
30import android.content.res.ObbInfo;
31import android.content.res.ObbScanner;
32import android.net.Uri;
33import android.os.Environment;
34import android.os.FileUtils;
35import android.os.IBinder;
36import android.os.ParcelFileDescriptor;
37import android.os.Process;
38import android.os.RemoteException;
39import android.os.ServiceManager;
40import android.os.StatFs;
41import android.provider.Settings;
42import android.util.DisplayMetrics;
43import android.util.Slog;
44
45import java.io.BufferedInputStream;
46import java.io.File;
47import java.io.FileInputStream;
48import java.io.FileNotFoundException;
49import java.io.IOException;
50import java.io.InputStream;
51import java.io.OutputStream;
52
53import libcore.io.ErrnoException;
54import libcore.io.Libcore;
55import libcore.io.StructStatFs;
56
57/*
58 * This service copies a downloaded apk to a file passed in as
59 * a ParcelFileDescriptor or to a newly created container specified
60 * by parameters. The DownloadManager gives access to this process
61 * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
62 * permission to access apks downloaded via the download manager.
63 */
64public class DefaultContainerService extends IntentService {
65    private static final String TAG = "DefContainer";
66    private static final boolean localLOGV = true;
67
68    private static final String LIB_DIR_NAME = "lib";
69
70    private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
71        /*
72         * Creates a new container and copies resource there.
73         * @param paackageURI the uri of resource to be copied. Can be either
74         * a content uri or a file uri
75         * @param cid the id of the secure container that should
76         * be used for creating a secure container into which the resource
77         * will be copied.
78         * @param key Refers to key used for encrypting the secure container
79         * @param resFileName Name of the target resource file(relative to newly
80         * created secure container)
81         * @return Returns the new cache path where the resource has been copied into
82         *
83         */
84        public String copyResourceToContainer(final Uri packageURI, final String cid,
85                final String key, final String resFileName, final String publicResFileName,
86                boolean isExternal, boolean isForwardLocked) {
87            if (packageURI == null || cid == null) {
88                return null;
89            }
90
91            return copyResourceInner(packageURI, cid, key, resFileName, publicResFileName,
92                    isExternal, isForwardLocked);
93        }
94
95        /*
96         * Copy specified resource to output stream
97         * @param packageURI the uri of resource to be copied. Should be a file
98         * uri
99         * @param outStream Remote file descriptor to be used for copying
100         * @return returns status code according to those in {@link
101         * PackageManager}
102         */
103        public int copyResource(final Uri packageURI, ParcelFileDescriptor outStream) {
104            if (packageURI == null || outStream == null) {
105                return PackageManager.INSTALL_FAILED_INVALID_URI;
106            }
107
108            ParcelFileDescriptor.AutoCloseOutputStream autoOut
109                    = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
110
111            try {
112                copyFile(packageURI, autoOut);
113                return PackageManager.INSTALL_SUCCEEDED;
114            } catch (FileNotFoundException e) {
115                Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " FNF: "
116                        + e.getMessage());
117                return PackageManager.INSTALL_FAILED_INVALID_URI;
118            } catch (IOException e) {
119                Slog.e(TAG, "Could not copy URI " + packageURI.toString() + " IO: "
120                        + e.getMessage());
121                return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
122            }
123        }
124
125        /*
126         * Determine the recommended install location for package
127         * specified by file uri location.
128         * @param fileUri the uri of resource to be copied. Should be a
129         * file uri
130         * @return Returns PackageInfoLite object containing
131         * the package info and recommended app location.
132         */
133        public PackageInfoLite getMinimalPackageInfo(final Uri fileUri, int flags, long threshold) {
134            PackageInfoLite ret = new PackageInfoLite();
135            if (fileUri == null) {
136                Slog.i(TAG, "Invalid package uri " + fileUri);
137                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
138                return ret;
139            }
140            String scheme = fileUri.getScheme();
141            if (scheme != null && !scheme.equals("file")) {
142                Slog.w(TAG, "Falling back to installing on internal storage only");
143                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_INSTALL_INTERNAL;
144                return ret;
145            }
146            String archiveFilePath = fileUri.getPath();
147            DisplayMetrics metrics = new DisplayMetrics();
148            metrics.setToDefaults();
149
150            PackageParser.PackageLite pkg = PackageParser.parsePackageLite(archiveFilePath, 0);
151            if (pkg == null) {
152                Slog.w(TAG, "Failed to parse package");
153
154                final File apkFile = new File(archiveFilePath);
155                if (!apkFile.exists()) {
156                    ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;
157                } else {
158                    ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
159                }
160
161                return ret;
162            }
163            ret.packageName = pkg.packageName;
164            ret.installLocation = pkg.installLocation;
165            ret.verifiers = pkg.verifiers;
166
167            ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation,
168                    archiveFilePath, flags, threshold);
169
170            return ret;
171        }
172
173        @Override
174        public boolean checkInternalFreeStorage(Uri packageUri, boolean isForwardLocked,
175                long threshold) throws RemoteException {
176            final File apkFile = new File(packageUri.getPath());
177            try {
178                return isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
179            } catch (IOException e) {
180                return true;
181            }
182        }
183
184        @Override
185        public boolean checkExternalFreeStorage(Uri packageUri, boolean isForwardLocked)
186                throws RemoteException {
187            final File apkFile = new File(packageUri.getPath());
188            try {
189                return isUnderExternalThreshold(apkFile, isForwardLocked);
190            } catch (IOException e) {
191                return true;
192            }
193        }
194
195        public ObbInfo getObbInfo(String filename) {
196            try {
197                return ObbScanner.getObbInfo(filename);
198            } catch (IOException e) {
199                Slog.d(TAG, "Couldn't get OBB info for " + filename);
200                return null;
201            }
202        }
203
204        @Override
205        public long calculateDirectorySize(String path) throws RemoteException {
206            final File directory = new File(path);
207            if (directory.exists() && directory.isDirectory()) {
208                return MeasurementUtils.measureDirectory(path);
209            } else {
210                return 0L;
211            }
212        }
213
214        @Override
215        public long[] getFileSystemStats(String path) {
216            try {
217                final StructStatFs stat = Libcore.os.statfs(path);
218                final long totalSize = stat.f_blocks * stat.f_bsize;
219                final long availSize = stat.f_bavail * stat.f_bsize;
220                return new long[] { totalSize, availSize };
221            } catch (ErrnoException e) {
222                throw new IllegalStateException(e);
223            }
224        }
225    };
226
227    public DefaultContainerService() {
228        super("DefaultContainerService");
229        setIntentRedelivery(true);
230    }
231
232    @Override
233    protected void onHandleIntent(Intent intent) {
234        if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
235            IPackageManager pm = IPackageManager.Stub.asInterface(
236                    ServiceManager.getService("package"));
237            String pkg = null;
238            try {
239                while ((pkg=pm.nextPackageToClean(pkg)) != null) {
240                    eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
241                    eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
242                    eraseFiles(Environment.getExternalStorageAppObbDirectory(pkg));
243                }
244            } catch (RemoteException e) {
245            }
246        }
247    }
248
249    void eraseFiles(File path) {
250        if (path.isDirectory()) {
251            String[] files = path.list();
252            if (files != null) {
253                for (String file : files) {
254                    eraseFiles(new File(path, file));
255                }
256            }
257        }
258        path.delete();
259    }
260
261    public IBinder onBind(Intent intent) {
262        return mBinder;
263    }
264
265    private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName,
266            String publicResFileName, boolean isExternal, boolean isForwardLocked) {
267
268        if (isExternal) {
269            // Make sure the sdcard is mounted.
270            String status = Environment.getExternalStorageState();
271            if (!status.equals(Environment.MEDIA_MOUNTED)) {
272                Slog.w(TAG, "Make sure sdcard is mounted.");
273                return null;
274            }
275        }
276
277        // The .apk file
278        String codePath = packageURI.getPath();
279        File codeFile = new File(codePath);
280
281        // Calculate size of container needed to hold base APK.
282        final int sizeMb;
283        try {
284            sizeMb = calculateContainerSize(codeFile, isForwardLocked);
285        } catch (IOException e) {
286            Slog.w(TAG, "Problem when trying to copy " + codeFile.getPath());
287            return null;
288        }
289
290        // Create new container
291        final String newCachePath = PackageHelper.createSdDir(sizeMb, newCid, key, Process.myUid(),
292                isExternal);
293        if (newCachePath == null) {
294            Slog.e(TAG, "Failed to create container " + newCid);
295            return null;
296        }
297
298        if (localLOGV) {
299            Slog.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
300        }
301
302        final File resFile = new File(newCachePath, resFileName);
303        if (FileUtils.copyFile(new File(codePath), resFile)) {
304            if (localLOGV) {
305                Slog.i(TAG, "Copied " + codePath + " to " + resFile);
306            }
307        } else {
308            Slog.e(TAG, "Failed to copy " + codePath + " to " + resFile);
309            // Clean up container
310            PackageHelper.destroySdDir(newCid);
311            return null;
312        }
313
314        if (isForwardLocked) {
315            File publicZipFile = new File(newCachePath, publicResFileName);
316            try {
317                PackageHelper.extractPublicFiles(resFile.getAbsolutePath(), publicZipFile);
318                if (localLOGV) {
319                    Slog.i(TAG, "Copied resources to " + publicZipFile);
320                }
321            } catch (IOException e) {
322                Slog.e(TAG, "Could not chown public APK " + publicZipFile.getAbsolutePath() + ": "
323                        + e.getMessage());
324                PackageHelper.destroySdDir(newCid);
325                return null;
326            }
327
328            try {
329                Libcore.os.chmod(resFile.getAbsolutePath(), 0640);
330                Libcore.os.chmod(publicZipFile.getAbsolutePath(), 0644);
331            } catch (ErrnoException e) {
332                Slog.e(TAG, "Could not chown APK or resource file: " + e.getMessage());
333                PackageHelper.destroySdDir(newCid);
334                return null;
335            }
336        }
337
338        final File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
339        if (sharedLibraryDir.mkdir()) {
340            int ret = NativeLibraryHelper.copyNativeBinariesIfNeededLI(codeFile, sharedLibraryDir);
341            if (ret != PackageManager.INSTALL_SUCCEEDED) {
342                Slog.e(TAG, "Could not copy native libraries to " + sharedLibraryDir.getPath());
343                PackageHelper.destroySdDir(newCid);
344                return null;
345            }
346        } else {
347            Slog.e(TAG, "Could not create native lib directory: " + sharedLibraryDir.getPath());
348            PackageHelper.destroySdDir(newCid);
349            return null;
350        }
351
352        if (!PackageHelper.finalizeSdDir(newCid)) {
353            Slog.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
354            // Clean up container
355            PackageHelper.destroySdDir(newCid);
356            return null;
357        }
358
359        if (localLOGV) {
360            Slog.i(TAG, "Finalized container " + newCid);
361        }
362
363        if (PackageHelper.isContainerMounted(newCid)) {
364            if (localLOGV) {
365                Slog.i(TAG, "Unmounting " + newCid + " at path " + newCachePath);
366            }
367
368            // Force a gc to avoid being killed.
369            Runtime.getRuntime().gc();
370            PackageHelper.unMountSdDir(newCid);
371        } else {
372            if (localLOGV) {
373                Slog.i(TAG, "Container " + newCid + " not mounted");
374            }
375        }
376
377        return newCachePath;
378    }
379
380    private static void copyToFile(InputStream inputStream, OutputStream out) throws IOException {
381        byte[] buffer = new byte[16384];
382        int bytesRead;
383        while ((bytesRead = inputStream.read(buffer)) >= 0) {
384            out.write(buffer, 0, bytesRead);
385        }
386    }
387
388    private static void copyToFile(File srcFile, OutputStream out)
389            throws FileNotFoundException, IOException {
390        InputStream inputStream = new BufferedInputStream(new FileInputStream(srcFile));
391        try {
392            copyToFile(inputStream, out);
393        } finally {
394            try { inputStream.close(); } catch (IOException e) {}
395        }
396    }
397
398    private void copyFile(Uri pPackageURI, OutputStream outStream) throws FileNotFoundException,
399            IOException {
400        String scheme = pPackageURI.getScheme();
401        if (scheme == null || scheme.equals("file")) {
402            final File srcPackageFile = new File(pPackageURI.getPath());
403            // We copy the source package file to a temp file and then rename it to the
404            // destination file in order to eliminate a window where the package directory
405            // scanner notices the new package file but it's not completely copied yet.
406            copyToFile(srcPackageFile, outStream);
407        } else if (scheme.equals("content")) {
408            ParcelFileDescriptor fd = null;
409            try {
410                fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
411            } catch (FileNotFoundException e) {
412                Slog.e(TAG, "Couldn't open file descriptor from download service. "
413                        + "Failed with exception " + e);
414                throw e;
415            }
416
417            if (fd == null) {
418                Slog.e(TAG, "Provider returned no file descriptor for " + pPackageURI.toString());
419                throw new FileNotFoundException("provider returned no file descriptor");
420            } else {
421                if (localLOGV) {
422                    Slog.i(TAG, "Opened file descriptor from download service.");
423                }
424                ParcelFileDescriptor.AutoCloseInputStream dlStream
425                        = new ParcelFileDescriptor.AutoCloseInputStream(fd);
426
427                // We copy the source package file to a temp file and then rename it to the
428                // destination file in order to eliminate a window where the package directory
429                // scanner notices the new package file but it's not completely
430                // copied
431                copyToFile(dlStream, outStream);
432            }
433        } else {
434            Slog.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
435            throw new FileNotFoundException("Package URI is not 'file:' or 'content:'");
436        }
437    }
438
439    private static final int PREFER_INTERNAL = 1;
440    private static final int PREFER_EXTERNAL = 2;
441
442    private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags,
443            long threshold) {
444        int prefer;
445        boolean checkBoth = false;
446
447        final boolean isForwardLocked = (flags & PackageManager.INSTALL_FORWARD_LOCK) != 0;
448
449        check_inner : {
450            /*
451             * Explicit install flags should override the manifest settings.
452             */
453            if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
454                prefer = PREFER_INTERNAL;
455                break check_inner;
456            } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
457                prefer = PREFER_EXTERNAL;
458                break check_inner;
459            }
460
461            /* No install flags. Check for manifest option. */
462            if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
463                prefer = PREFER_INTERNAL;
464                break check_inner;
465            } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
466                prefer = PREFER_EXTERNAL;
467                checkBoth = true;
468                break check_inner;
469            } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
470                // We default to preferring internal storage.
471                prefer = PREFER_INTERNAL;
472                checkBoth = true;
473                break check_inner;
474            }
475
476            // Pick user preference
477            int installPreference = Settings.System.getInt(getApplicationContext()
478                    .getContentResolver(),
479                    Settings.Secure.DEFAULT_INSTALL_LOCATION,
480                    PackageHelper.APP_INSTALL_AUTO);
481            if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
482                prefer = PREFER_INTERNAL;
483                break check_inner;
484            } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
485                prefer = PREFER_EXTERNAL;
486                break check_inner;
487            }
488
489            /*
490             * Fall back to default policy of internal-only if nothing else is
491             * specified.
492             */
493            prefer = PREFER_INTERNAL;
494        }
495
496        final boolean emulated = Environment.isExternalStorageEmulated();
497
498        final File apkFile = new File(archiveFilePath);
499
500        boolean fitsOnInternal = false;
501        if (checkBoth || prefer == PREFER_INTERNAL) {
502            try {
503                fitsOnInternal = isUnderInternalThreshold(apkFile, isForwardLocked, threshold);
504            } catch (IOException e) {
505                return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
506            }
507        }
508
509        boolean fitsOnSd = false;
510        if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
511            try {
512                fitsOnSd = isUnderExternalThreshold(apkFile, isForwardLocked);
513            } catch (IOException e) {
514                return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
515            }
516        }
517
518        if (prefer == PREFER_INTERNAL) {
519            if (fitsOnInternal) {
520                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
521            }
522        } else if (!emulated && prefer == PREFER_EXTERNAL) {
523            if (fitsOnSd) {
524                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
525            }
526        }
527
528        if (checkBoth) {
529            if (fitsOnInternal) {
530                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
531            } else if (!emulated && fitsOnSd) {
532                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
533            }
534        }
535
536        /*
537         * If they requested to be on the external media by default, return that
538         * the media was unavailable. Otherwise, indicate there was insufficient
539         * storage space available.
540         */
541        if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)
542                && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
543            return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
544        } else {
545            return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
546        }
547    }
548
549    /**
550     * Measure a file to see if it fits within the free space threshold.
551     *
552     * @param apkFile file to check
553     * @param threshold byte threshold to compare against
554     * @return true if file fits under threshold
555     * @throws FileNotFoundException when APK does not exist
556     */
557    private boolean isUnderInternalThreshold(File apkFile, boolean isForwardLocked, long threshold)
558            throws IOException {
559        long size = apkFile.length();
560        if (size == 0 && !apkFile.exists()) {
561            throw new FileNotFoundException();
562        }
563
564        if (isForwardLocked) {
565            size += PackageHelper.extractPublicFiles(apkFile.getAbsolutePath(), null);
566        }
567
568        final StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
569        final long availInternalSize = (long) internalStats.getAvailableBlocks()
570                * (long) internalStats.getBlockSize();
571
572        return (availInternalSize - size) > threshold;
573    }
574
575
576    /**
577     * Measure a file to see if it fits in the external free space.
578     *
579     * @param apkFile file to check
580     * @return true if file fits
581     * @throws IOException when file does not exist
582     */
583    private boolean isUnderExternalThreshold(File apkFile, boolean isForwardLocked)
584            throws IOException {
585        if (Environment.isExternalStorageEmulated()) {
586            return false;
587        }
588
589        final int sizeMb = calculateContainerSize(apkFile, isForwardLocked);
590
591        final int availSdMb;
592        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
593            final StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
594            final int blocksToMb = (1 << 20) / sdStats.getBlockSize();
595            availSdMb = sdStats.getAvailableBlocks() * blocksToMb;
596        } else {
597            availSdMb = -1;
598        }
599
600        return availSdMb > sizeMb;
601    }
602
603    /**
604     * Calculate the container size for an APK. Takes into account the
605     *
606     * @param apkFile file from which to calculate size
607     * @return size in megabytes (2^20 bytes)
608     * @throws IOException when there is a problem reading the file
609     */
610    private int calculateContainerSize(File apkFile, boolean forwardLocked) throws IOException {
611        // Calculate size of container needed to hold base APK.
612        long sizeBytes = apkFile.length();
613        if (sizeBytes == 0 && !apkFile.exists()) {
614            throw new FileNotFoundException();
615        }
616
617        // Check all the native files that need to be copied and add that to the
618        // container size.
619        sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(apkFile);
620
621        if (forwardLocked) {
622            sizeBytes += PackageHelper.extractPublicFiles(apkFile.getPath(), null);
623        }
624
625        int sizeMb = (int) (sizeBytes >> 20);
626        if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) {
627            sizeMb++;
628        }
629
630        /*
631         * Add buffer size because we don't have a good way to determine the
632         * real FAT size. Your FAT size varies with how many directory entries
633         * you need, how big the whole filesystem is, and other such headaches.
634         */
635        sizeMb++;
636
637        return sizeMb;
638    }
639}
640