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