DefaultContainerService.java revision 300c13a48132f03d48462b9cd3ec41331a71a411
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.content.Intent;
24import android.content.pm.IPackageManager;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageInfoLite;
27import android.content.pm.PackageManager;
28import android.content.pm.PackageParser;
29import android.content.res.ObbInfo;
30import android.content.res.ObbScanner;
31import android.net.Uri;
32import android.os.Environment;
33import android.os.IBinder;
34import android.os.ParcelFileDescriptor;
35import android.os.Process;
36import android.os.RemoteException;
37import android.os.ServiceManager;
38import android.os.StatFs;
39import android.app.IntentService;
40import android.util.DisplayMetrics;
41import android.util.Log;
42import android.util.Pair;
43
44import java.io.File;
45import java.io.FileInputStream;
46import java.io.FileNotFoundException;
47import java.io.FileOutputStream;
48import java.io.IOException;
49import java.io.InputStream;
50import java.util.LinkedList;
51import java.util.List;
52import java.util.zip.ZipEntry;
53import java.util.zip.ZipException;
54import java.util.zip.ZipFile;
55
56import android.os.FileUtils;
57import android.provider.Settings;
58
59/*
60 * This service copies a downloaded apk to a file passed in as
61 * a ParcelFileDescriptor or to a newly created container specified
62 * by parameters. The DownloadManager gives access to this process
63 * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
64 * permission to access apks downloaded via the download manager.
65 */
66public class DefaultContainerService extends IntentService {
67    private static final String TAG = "DefContainer";
68    private static final boolean localLOGV = true;
69
70    private static final String LIB_DIR_NAME = "lib";
71
72    private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
73        /*
74         * Creates a new container and copies resource there.
75         * @param paackageURI the uri of resource to be copied. Can be either
76         * a content uri or a file uri
77         * @param cid the id of the secure container that should
78         * be used for creating a secure container into which the resource
79         * will be copied.
80         * @param key Refers to key used for encrypting the secure container
81         * @param resFileName Name of the target resource file(relative to newly
82         * created secure container)
83         * @return Returns the new cache path where the resource has been copied into
84         *
85         */
86        public String copyResourceToContainer(final Uri packageURI,
87                final String cid,
88                final String key, final String resFileName) {
89            if (packageURI == null || cid == null) {
90                return null;
91            }
92            return copyResourceInner(packageURI, cid, key, resFileName);
93        }
94
95        /*
96         * Copy specified resource to output stream
97         * @param packageURI the uri of resource to be copied. Should be a
98         * file uri
99         * @param outStream Remote file descriptor to be used for copying
100         * @return Returns true if copy succeded or false otherwise.
101         */
102        public boolean copyResource(final Uri packageURI,
103                ParcelFileDescriptor outStream) {
104            if (packageURI == null ||  outStream == null) {
105                return false;
106            }
107            ParcelFileDescriptor.AutoCloseOutputStream
108            autoOut = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
109            return copyFile(packageURI, autoOut);
110        }
111
112        /*
113         * Determine the recommended install location for package
114         * specified by file uri location.
115         * @param fileUri the uri of resource to be copied. Should be a
116         * file uri
117         * @return Returns PackageInfoLite object containing
118         * the package info and recommended app location.
119         */
120        public PackageInfoLite getMinimalPackageInfo(final Uri fileUri, int flags) {
121            PackageInfoLite ret = new PackageInfoLite();
122            if (fileUri == null) {
123                Log.i(TAG, "Invalid package uri " + fileUri);
124                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
125                return ret;
126            }
127            String scheme = fileUri.getScheme();
128            if (scheme != null && !scheme.equals("file")) {
129                Log.w(TAG, "Falling back to installing on internal storage only");
130                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_INSTALL_INTERNAL;
131                return ret;
132            }
133            String archiveFilePath = fileUri.getPath();
134            PackageParser packageParser = new PackageParser(archiveFilePath);
135            File sourceFile = new File(archiveFilePath);
136            DisplayMetrics metrics = new DisplayMetrics();
137            metrics.setToDefaults();
138            PackageParser.PackageLite pkg = packageParser.parsePackageLite(
139                    archiveFilePath, 0);
140            // Nuke the parser reference right away and force a gc
141            packageParser = null;
142            Runtime.getRuntime().gc();
143            if (pkg == null) {
144                Log.w(TAG, "Failed to parse package");
145                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
146                return ret;
147            }
148            ret.packageName = pkg.packageName;
149            ret.installLocation = pkg.installLocation;
150            ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation, archiveFilePath, flags);
151            return ret;
152        }
153
154        public boolean checkFreeStorage(boolean external, Uri fileUri) {
155            return checkFreeStorageInner(external, fileUri);
156        }
157
158        public ObbInfo getObbInfo(String filename) {
159            try {
160                return ObbScanner.getObbInfo(filename);
161            } catch (IOException e) {
162                Log.d(TAG, "Couldn't get OBB info for " + filename);
163                return null;
164            }
165        }
166
167        @Override
168        public long calculateDirectorySize(String path) throws RemoteException {
169            final File directory = new File(path);
170            if (directory.exists() && directory.isDirectory()) {
171                return MeasurementUtils.measureDirectory(path);
172            } else {
173                return 0L;
174            }
175        }
176    };
177
178    public DefaultContainerService() {
179        super("DefaultContainerService");
180        setIntentRedelivery(true);
181    }
182
183    @Override
184    protected void onHandleIntent(Intent intent) {
185        if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
186            IPackageManager pm = IPackageManager.Stub.asInterface(
187                    ServiceManager.getService("package"));
188            String pkg = null;
189            try {
190                while ((pkg=pm.nextPackageToClean(pkg)) != null) {
191                    eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
192                    eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
193                    eraseFiles(Environment.getExternalStorageAppObbDirectory(pkg));
194                }
195            } catch (RemoteException e) {
196            }
197        }
198    }
199
200    void eraseFiles(File path) {
201        if (path.isDirectory()) {
202            String[] files = path.list();
203            if (files != null) {
204                for (String file : files) {
205                    eraseFiles(new File(path, file));
206                }
207            }
208        }
209        path.delete();
210    }
211
212    public IBinder onBind(Intent intent) {
213        return mBinder;
214    }
215
216    private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName) {
217        // Make sure the sdcard is mounted.
218        String status = Environment.getExternalStorageState();
219        if (!status.equals(Environment.MEDIA_MOUNTED)) {
220            Log.w(TAG, "Make sure sdcard is mounted.");
221            return null;
222        }
223
224        // The .apk file
225        String codePath = packageURI.getPath();
226        File codeFile = new File(codePath);
227
228        // Calculate size of container needed to hold base APK.
229        long sizeBytes = codeFile.length();
230
231        // Check all the native files that need to be copied and add that to the container size.
232        ZipFile zipFile;
233        List<Pair<ZipEntry, String>> nativeFiles;
234        try {
235            zipFile = new ZipFile(codeFile);
236
237            nativeFiles = new LinkedList<Pair<ZipEntry, String>>();
238
239            NativeLibraryHelper.listPackageNativeBinariesLI(zipFile, nativeFiles);
240
241            final int N = nativeFiles.size();
242            for (int i = 0; i < N; i++) {
243                final Pair<ZipEntry, String> entry = nativeFiles.get(i);
244
245                /*
246                 * Note that PackageHelper.createSdDir adds a 1MB padding on
247                 * our claimed size, so we don't have to worry about block
248                 * alignment here.
249                 */
250                sizeBytes += entry.first.getSize();
251            }
252        } catch (ZipException e) {
253            Log.w(TAG, "Failed to extract data from package file", e);
254            return null;
255        } catch (IOException e) {
256            Log.w(TAG, "Failed to cache package shared libs", e);
257            return null;
258        }
259
260        // Create new container
261        String newCachePath = null;
262        if ((newCachePath = PackageHelper.createSdDir(sizeBytes, newCid, key, Process.myUid())) == null) {
263            Log.e(TAG, "Failed to create container " + newCid);
264            return null;
265        }
266        if (localLOGV)
267            Log.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
268        File resFile = new File(newCachePath, resFileName);
269        if (!FileUtils.copyFile(new File(codePath), resFile)) {
270            Log.e(TAG, "Failed to copy " + codePath + " to " + resFile);
271            // Clean up container
272            PackageHelper.destroySdDir(newCid);
273            return null;
274        }
275
276        try {
277            File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
278            sharedLibraryDir.mkdir();
279
280            final int N = nativeFiles.size();
281            for (int i = 0; i < N; i++) {
282                final Pair<ZipEntry, String> entry = nativeFiles.get(i);
283
284                InputStream is = zipFile.getInputStream(entry.first);
285                try {
286                    File destFile = new File(sharedLibraryDir, entry.second);
287                    if (!FileUtils.copyToFile(is, destFile)) {
288                        throw new IOException("Couldn't copy native binary "
289                                + entry.first.getName() + " to " + entry.second);
290                    }
291                } finally {
292                    is.close();
293                }
294            }
295        } catch (IOException e) {
296            Log.e(TAG, "Couldn't copy native file to container", e);
297            PackageHelper.destroySdDir(newCid);
298            return null;
299        }
300
301        if (localLOGV) Log.i(TAG, "Copied " + codePath + " to " + resFile);
302        if (!PackageHelper.finalizeSdDir(newCid)) {
303            Log.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
304            // Clean up container
305            PackageHelper.destroySdDir(newCid);
306        }
307        if (localLOGV) Log.i(TAG, "Finalized container " + newCid);
308        if (PackageHelper.isContainerMounted(newCid)) {
309            if (localLOGV) Log.i(TAG, "Unmounting " + newCid +
310                    " at path " + newCachePath);
311            // Force a gc to avoid being killed.
312            Runtime.getRuntime().gc();
313            PackageHelper.unMountSdDir(newCid);
314        } else {
315            if (localLOGV) Log.i(TAG, "Container " + newCid + " not mounted");
316        }
317        return newCachePath;
318    }
319
320    public static boolean copyToFile(InputStream inputStream, FileOutputStream out) {
321        try {
322            byte[] buffer = new byte[4096];
323            int bytesRead;
324            while ((bytesRead = inputStream.read(buffer)) >= 0) {
325                out.write(buffer, 0, bytesRead);
326            }
327            return true;
328        } catch (IOException e) {
329            Log.i(TAG, "Exception : " + e + " when copying file");
330            return false;
331        }
332    }
333
334    public static boolean copyToFile(File srcFile, FileOutputStream out) {
335        InputStream inputStream = null;
336        try {
337            inputStream = new FileInputStream(srcFile);
338            return copyToFile(inputStream, out);
339        } catch (IOException e) {
340            return false;
341        } finally {
342            try { if (inputStream != null) inputStream.close(); } catch (IOException e) {}
343        }
344    }
345
346    private  boolean copyFile(Uri pPackageURI, FileOutputStream outStream) {
347        String scheme = pPackageURI.getScheme();
348        if (scheme == null || scheme.equals("file")) {
349            final File srcPackageFile = new File(pPackageURI.getPath());
350            // We copy the source package file to a temp file and then rename it to the
351            // destination file in order to eliminate a window where the package directory
352            // scanner notices the new package file but it's not completely copied yet.
353            if (!copyToFile(srcPackageFile, outStream)) {
354                Log.e(TAG, "Couldn't copy file: " + srcPackageFile);
355                return false;
356            }
357        } else if (scheme.equals("content")) {
358            ParcelFileDescriptor fd = null;
359            try {
360                fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
361            } catch (FileNotFoundException e) {
362                Log.e(TAG, "Couldn't open file descriptor from download service. Failed with exception " + e);
363                return false;
364            }
365            if (fd == null) {
366                Log.e(TAG, "Couldn't open file descriptor from download service (null).");
367                return false;
368            } else {
369                if (localLOGV) {
370                    Log.v(TAG, "Opened file descriptor from download service.");
371                }
372                ParcelFileDescriptor.AutoCloseInputStream
373                dlStream = new ParcelFileDescriptor.AutoCloseInputStream(fd);
374                // We copy the source package file to a temp file and then rename it to the
375                // destination file in order to eliminate a window where the package directory
376                // scanner notices the new package file but it's not completely copied yet.
377                if (!copyToFile(dlStream, outStream)) {
378                    Log.e(TAG, "Couldn't copy " + pPackageURI + " to temp file.");
379                    return false;
380                }
381            }
382        } else {
383            Log.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
384            return false;
385        }
386        return true;
387    }
388
389    // Constants related to app heuristics
390    // No-installation limit for internal flash: 10% or less space available
391    private static final double LOW_NAND_FLASH_TRESHOLD = 0.1;
392
393    // SD-to-internal app size threshold: currently set to 1 MB
394    private static final long INSTALL_ON_SD_THRESHOLD = (1024 * 1024);
395    private static final int ERR_LOC = -1;
396
397    private int recommendAppInstallLocation(int installLocation,
398            String archiveFilePath, int flags) {
399        boolean checkInt = false;
400        boolean checkExt = false;
401        boolean checkBoth = false;
402        check_inner : {
403            // Check flags.
404            if ((flags & PackageManager.INSTALL_FORWARD_LOCK) != 0) {
405                // Check for forward locked app
406                checkInt = true;
407                break check_inner;
408            } else if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
409                // Explicit flag to install internally.
410                // Check internal storage and return
411                checkInt = true;
412                break check_inner;
413            } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
414                // Explicit flag to install externally.
415                // Check external storage and return
416                checkExt = true;
417                break check_inner;
418            }
419            // Check for manifest option
420            if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
421                checkInt = true;
422                break check_inner;
423            } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
424                checkExt = true;
425                checkBoth = true;
426                break check_inner;
427            } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
428                checkInt = true;
429                checkBoth = true;
430                break check_inner;
431            }
432            // Pick user preference
433            int installPreference = Settings.System.getInt(getApplicationContext()
434                    .getContentResolver(),
435                    Settings.Secure.DEFAULT_INSTALL_LOCATION,
436                    PackageHelper.APP_INSTALL_AUTO);
437            if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
438                checkInt = true;
439                break check_inner;
440            } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
441                checkExt = true;
442                break check_inner;
443            }
444            // Fall back to default policy if nothing else is specified.
445            checkInt = true;
446        }
447
448        // Package size = code size + cache size + data size
449        // If code size > 1 MB, install on SD card.
450        // Else install on internal NAND flash, unless space on NAND is less than 10%
451        String status = Environment.getExternalStorageState();
452        long availSDSize = -1;
453        boolean mediaAvailable = false;
454        if (!Environment.isExternalStorageEmulated() && status.equals(Environment.MEDIA_MOUNTED)) {
455            StatFs sdStats = new StatFs(
456                    Environment.getExternalStorageDirectory().getPath());
457            availSDSize = (long)sdStats.getAvailableBlocks() *
458                    (long)sdStats.getBlockSize();
459            mediaAvailable = true;
460        }
461        StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
462        long totalInternalSize = (long)internalStats.getBlockCount() *
463                (long)internalStats.getBlockSize();
464        long availInternalSize = (long)internalStats.getAvailableBlocks() *
465                (long)internalStats.getBlockSize();
466
467        double pctNandFree = (double)availInternalSize / (double)totalInternalSize;
468
469        File apkFile = new File(archiveFilePath);
470        long pkgLen = apkFile.length();
471
472        // To make final copy
473        long reqInstallSize = pkgLen;
474        // For dex files. Just ignore and fail when extracting. Max limit of 2Gig for now.
475        long reqInternalSize = 0;
476        boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
477        boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalSize);
478        boolean fitsOnSd = false;
479        if (mediaAvailable && (reqInstallSize < availSDSize)) {
480            // If we do not have an internal size requirement
481            // don't do a threshold check.
482            if (reqInternalSize == 0) {
483                fitsOnSd = true;
484            } else if ((reqInternalSize < availInternalSize) && intThresholdOk) {
485                fitsOnSd = true;
486            }
487        }
488        boolean fitsOnInt = intThresholdOk && intAvailOk;
489        if (checkInt) {
490            // Check for internal memory availability
491            if (fitsOnInt) {
492                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
493            }
494        } else if (checkExt) {
495            if (fitsOnSd) {
496                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
497            }
498        }
499        if (checkBoth) {
500            // Check for internal first
501            if (fitsOnInt) {
502                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
503            }
504            // Check for external next
505            if (fitsOnSd) {
506                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
507            }
508        }
509        if ((checkExt || checkBoth) && !mediaAvailable) {
510            return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
511        }
512        return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
513    }
514
515    private boolean checkFreeStorageInner(boolean external, Uri packageURI) {
516        File apkFile = new File(packageURI.getPath());
517        long size = apkFile.length();
518        if (external) {
519            String status = Environment.getExternalStorageState();
520            long availSDSize = -1;
521            if (status.equals(Environment.MEDIA_MOUNTED)) {
522                StatFs sdStats = new StatFs(
523                        Environment.getExternalStorageDirectory().getPath());
524                availSDSize = (long)sdStats.getAvailableBlocks() *
525                (long)sdStats.getBlockSize();
526            }
527            return availSDSize > size;
528        }
529        StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
530        long totalInternalSize = (long)internalStats.getBlockCount() *
531        (long)internalStats.getBlockSize();
532        long availInternalSize = (long)internalStats.getAvailableBlocks() *
533        (long)internalStats.getBlockSize();
534
535        double pctNandFree = (double)availInternalSize / (double)totalInternalSize;
536        // To make final copy
537        long reqInstallSize = size;
538        // For dex files. Just ignore and fail when extracting. Max limit of 2Gig for now.
539        long reqInternalSize = 0;
540        boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
541        boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalSize);
542        return intThresholdOk && intAvailOk;
543    }
544}
545