DefaultContainerService.java revision 1ebd74acf9977daa42133507e970dab88e08f0ef
10c1bc742181ded4930842b46e9507372f0b1b963James Dong/*
20c1bc742181ded4930842b46e9507372f0b1b963James Dong * Copyright (C) 2010 The Android Open Source Project
30c1bc742181ded4930842b46e9507372f0b1b963James Dong *
40c1bc742181ded4930842b46e9507372f0b1b963James Dong * Licensed under the Apache License, Version 2.0 (the "License");
50c1bc742181ded4930842b46e9507372f0b1b963James Dong * you may not use this file except in compliance with the License.
60c1bc742181ded4930842b46e9507372f0b1b963James Dong * You may obtain a copy of the License at
70c1bc742181ded4930842b46e9507372f0b1b963James Dong *
80c1bc742181ded4930842b46e9507372f0b1b963James Dong *      http://www.apache.org/licenses/LICENSE-2.0
90c1bc742181ded4930842b46e9507372f0b1b963James Dong *
100c1bc742181ded4930842b46e9507372f0b1b963James Dong * Unless required by applicable law or agreed to in writing, software
110c1bc742181ded4930842b46e9507372f0b1b963James Dong * distributed under the License is distributed on an "AS IS" BASIS,
120c1bc742181ded4930842b46e9507372f0b1b963James Dong * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
130c1bc742181ded4930842b46e9507372f0b1b963James Dong * See the License for the specific language governing permissions and
140c1bc742181ded4930842b46e9507372f0b1b963James Dong * limitations under the License.
150c1bc742181ded4930842b46e9507372f0b1b963James Dong */
160c1bc742181ded4930842b46e9507372f0b1b963James Dong
170c1bc742181ded4930842b46e9507372f0b1b963James Dongpackage com.android.defcontainer;
180c1bc742181ded4930842b46e9507372f0b1b963James Dong
190c1bc742181ded4930842b46e9507372f0b1b963James Dongimport com.android.internal.app.IMediaContainerService;
200c1bc742181ded4930842b46e9507372f0b1b963James Dongimport com.android.internal.content.NativeLibraryHelper;
210c1bc742181ded4930842b46e9507372f0b1b963James Dongimport com.android.internal.content.PackageHelper;
220c1bc742181ded4930842b46e9507372f0b1b963James Dong
230c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.content.Intent;
240c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.content.pm.IPackageManager;
250c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.content.pm.PackageInfo;
260c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.content.pm.PackageInfoLite;
270c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.content.pm.PackageManager;
280c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.content.pm.PackageParser;
290c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.content.res.ObbInfo;
300c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.content.res.ObbScanner;
310c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.net.Uri;
320c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.os.Environment;
330c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.os.IBinder;
340c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.os.ParcelFileDescriptor;
350c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.os.Process;
360c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.os.RemoteException;
370c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.os.ServiceManager;
380c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.os.StatFs;
390c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.app.IntentService;
400c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.util.DisplayMetrics;
410c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.util.Slog;
420c1bc742181ded4930842b46e9507372f0b1b963James Dong
430c1bc742181ded4930842b46e9507372f0b1b963James Dongimport java.io.File;
440c1bc742181ded4930842b46e9507372f0b1b963James Dongimport java.io.FileInputStream;
450c1bc742181ded4930842b46e9507372f0b1b963James Dongimport java.io.FileNotFoundException;
460c1bc742181ded4930842b46e9507372f0b1b963James Dongimport java.io.FileOutputStream;
470c1bc742181ded4930842b46e9507372f0b1b963James Dongimport java.io.IOException;
480c1bc742181ded4930842b46e9507372f0b1b963James Dongimport java.io.InputStream;
490c1bc742181ded4930842b46e9507372f0b1b963James Dong
500c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.os.FileUtils;
510c1bc742181ded4930842b46e9507372f0b1b963James Dongimport android.provider.Settings;
520c1bc742181ded4930842b46e9507372f0b1b963James Dong
530c1bc742181ded4930842b46e9507372f0b1b963James Dong/*
540c1bc742181ded4930842b46e9507372f0b1b963James Dong * This service copies a downloaded apk to a file passed in as
550c1bc742181ded4930842b46e9507372f0b1b963James Dong * a ParcelFileDescriptor or to a newly created container specified
560c1bc742181ded4930842b46e9507372f0b1b963James Dong * by parameters. The DownloadManager gives access to this process
570c1bc742181ded4930842b46e9507372f0b1b963James Dong * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
580c1bc742181ded4930842b46e9507372f0b1b963James Dong * permission to access apks downloaded via the download manager.
590c1bc742181ded4930842b46e9507372f0b1b963James Dong */
600c1bc742181ded4930842b46e9507372f0b1b963James Dongpublic class DefaultContainerService extends IntentService {
610c1bc742181ded4930842b46e9507372f0b1b963James Dong    private static final String TAG = "DefContainer";
620c1bc742181ded4930842b46e9507372f0b1b963James Dong    private static final boolean localLOGV = true;
630c1bc742181ded4930842b46e9507372f0b1b963James Dong
640c1bc742181ded4930842b46e9507372f0b1b963James Dong    private static final String LIB_DIR_NAME = "lib";
650c1bc742181ded4930842b46e9507372f0b1b963James Dong
660c1bc742181ded4930842b46e9507372f0b1b963James Dong    private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
670c1bc742181ded4930842b46e9507372f0b1b963James Dong        /*
680c1bc742181ded4930842b46e9507372f0b1b963James Dong         * Creates a new container and copies resource there.
690c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @param paackageURI the uri of resource to be copied. Can be either
700c1bc742181ded4930842b46e9507372f0b1b963James Dong         * a content uri or a file uri
710c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @param cid the id of the secure container that should
720c1bc742181ded4930842b46e9507372f0b1b963James Dong         * be used for creating a secure container into which the resource
730c1bc742181ded4930842b46e9507372f0b1b963James Dong         * will be copied.
740c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @param key Refers to key used for encrypting the secure container
750c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @param resFileName Name of the target resource file(relative to newly
760c1bc742181ded4930842b46e9507372f0b1b963James Dong         * created secure container)
770c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @return Returns the new cache path where the resource has been copied into
780c1bc742181ded4930842b46e9507372f0b1b963James Dong         *
790c1bc742181ded4930842b46e9507372f0b1b963James Dong         */
800c1bc742181ded4930842b46e9507372f0b1b963James Dong        public String copyResourceToContainer(final Uri packageURI,
810c1bc742181ded4930842b46e9507372f0b1b963James Dong                final String cid,
820c1bc742181ded4930842b46e9507372f0b1b963James Dong                final String key, final String resFileName) {
830c1bc742181ded4930842b46e9507372f0b1b963James Dong            if (packageURI == null || cid == null) {
840c1bc742181ded4930842b46e9507372f0b1b963James Dong                return null;
850c1bc742181ded4930842b46e9507372f0b1b963James Dong            }
860c1bc742181ded4930842b46e9507372f0b1b963James Dong            return copyResourceInner(packageURI, cid, key, resFileName);
870c1bc742181ded4930842b46e9507372f0b1b963James Dong        }
880c1bc742181ded4930842b46e9507372f0b1b963James Dong
890c1bc742181ded4930842b46e9507372f0b1b963James Dong        /*
900c1bc742181ded4930842b46e9507372f0b1b963James Dong         * Copy specified resource to output stream
910c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @param packageURI the uri of resource to be copied. Should be a
920c1bc742181ded4930842b46e9507372f0b1b963James Dong         * file uri
930c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @param outStream Remote file descriptor to be used for copying
940c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @return Returns true if copy succeded or false otherwise.
950c1bc742181ded4930842b46e9507372f0b1b963James Dong         */
960c1bc742181ded4930842b46e9507372f0b1b963James Dong        public boolean copyResource(final Uri packageURI,
970c1bc742181ded4930842b46e9507372f0b1b963James Dong                ParcelFileDescriptor outStream) {
980c1bc742181ded4930842b46e9507372f0b1b963James Dong            if (packageURI == null ||  outStream == null) {
990c1bc742181ded4930842b46e9507372f0b1b963James Dong                return false;
1000c1bc742181ded4930842b46e9507372f0b1b963James Dong            }
1010c1bc742181ded4930842b46e9507372f0b1b963James Dong            ParcelFileDescriptor.AutoCloseOutputStream
1020c1bc742181ded4930842b46e9507372f0b1b963James Dong            autoOut = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
1030c1bc742181ded4930842b46e9507372f0b1b963James Dong            return copyFile(packageURI, autoOut);
1040c1bc742181ded4930842b46e9507372f0b1b963James Dong        }
1050c1bc742181ded4930842b46e9507372f0b1b963James Dong
1060c1bc742181ded4930842b46e9507372f0b1b963James Dong        /*
1070c1bc742181ded4930842b46e9507372f0b1b963James Dong         * Determine the recommended install location for package
1080c1bc742181ded4930842b46e9507372f0b1b963James Dong         * specified by file uri location.
1090c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @param fileUri the uri of resource to be copied. Should be a
1100c1bc742181ded4930842b46e9507372f0b1b963James Dong         * file uri
1110c1bc742181ded4930842b46e9507372f0b1b963James Dong         * @return Returns PackageInfoLite object containing
1120c1bc742181ded4930842b46e9507372f0b1b963James Dong         * the package info and recommended app location.
1130c1bc742181ded4930842b46e9507372f0b1b963James Dong         */
1140c1bc742181ded4930842b46e9507372f0b1b963James Dong        public PackageInfoLite getMinimalPackageInfo(final Uri fileUri, int flags, long threshold) {
1150c1bc742181ded4930842b46e9507372f0b1b963James Dong            PackageInfoLite ret = new PackageInfoLite();
1160c1bc742181ded4930842b46e9507372f0b1b963James Dong            if (fileUri == null) {
1170c1bc742181ded4930842b46e9507372f0b1b963James Dong                Slog.i(TAG, "Invalid package uri " + fileUri);
1180c1bc742181ded4930842b46e9507372f0b1b963James Dong                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
1190c1bc742181ded4930842b46e9507372f0b1b963James Dong                return ret;
1200c1bc742181ded4930842b46e9507372f0b1b963James Dong            }
121            String scheme = fileUri.getScheme();
122            if (scheme != null && !scheme.equals("file")) {
123                Slog.w(TAG, "Falling back to installing on internal storage only");
124                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_INSTALL_INTERNAL;
125                return ret;
126            }
127            String archiveFilePath = fileUri.getPath();
128            DisplayMetrics metrics = new DisplayMetrics();
129            metrics.setToDefaults();
130
131            PackageParser.PackageLite pkg = PackageParser.parsePackageLite(archiveFilePath, 0);
132            if (pkg == null) {
133                Slog.w(TAG, "Failed to parse package");
134
135                final File apkFile = new File(archiveFilePath);
136                if (!apkFile.exists()) {
137                    ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;
138                } else {
139                    ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
140                }
141
142                return ret;
143            }
144            ret.packageName = pkg.packageName;
145            ret.installLocation = pkg.installLocation;
146
147            ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation,
148                    archiveFilePath, flags, threshold);
149
150            return ret;
151        }
152
153        @Override
154        public boolean checkInternalFreeStorage(Uri packageUri, long threshold)
155                throws RemoteException {
156            final File apkFile = new File(packageUri.getPath());
157            try {
158                return isUnderInternalThreshold(apkFile, threshold);
159            } catch (FileNotFoundException e) {
160                return true;
161            }
162        }
163
164        @Override
165        public boolean checkExternalFreeStorage(Uri packageUri) throws RemoteException {
166            final File apkFile = new File(packageUri.getPath());
167            try {
168                return isUnderExternalThreshold(apkFile);
169            } catch (FileNotFoundException e) {
170                return true;
171            }
172        }
173
174        public ObbInfo getObbInfo(String filename) {
175            try {
176                return ObbScanner.getObbInfo(filename);
177            } catch (IOException e) {
178                Slog.d(TAG, "Couldn't get OBB info for " + filename);
179                return null;
180            }
181        }
182
183        @Override
184        public long calculateDirectorySize(String path) throws RemoteException {
185            final File directory = new File(path);
186            if (directory.exists() && directory.isDirectory()) {
187                return MeasurementUtils.measureDirectory(path);
188            } else {
189                return 0L;
190            }
191        }
192    };
193
194    public DefaultContainerService() {
195        super("DefaultContainerService");
196        setIntentRedelivery(true);
197    }
198
199    @Override
200    protected void onHandleIntent(Intent intent) {
201        if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
202            IPackageManager pm = IPackageManager.Stub.asInterface(
203                    ServiceManager.getService("package"));
204            String pkg = null;
205            try {
206                while ((pkg=pm.nextPackageToClean(pkg)) != null) {
207                    eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
208                    eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
209                    eraseFiles(Environment.getExternalStorageAppObbDirectory(pkg));
210                }
211            } catch (RemoteException e) {
212            }
213        }
214    }
215
216    void eraseFiles(File path) {
217        if (path.isDirectory()) {
218            String[] files = path.list();
219            if (files != null) {
220                for (String file : files) {
221                    eraseFiles(new File(path, file));
222                }
223            }
224        }
225        path.delete();
226    }
227
228    public IBinder onBind(Intent intent) {
229        return mBinder;
230    }
231
232    private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName) {
233        // Make sure the sdcard is mounted.
234        String status = Environment.getExternalStorageState();
235        if (!status.equals(Environment.MEDIA_MOUNTED)) {
236            Slog.w(TAG, "Make sure sdcard is mounted.");
237            return null;
238        }
239
240        // The .apk file
241        String codePath = packageURI.getPath();
242        File codeFile = new File(codePath);
243
244        // Calculate size of container needed to hold base APK.
245        int sizeMb;
246        try {
247            sizeMb = calculateContainerSize(codeFile);
248        } catch (FileNotFoundException e) {
249            Slog.w(TAG, "File does not exist when trying to copy " + codeFile.getPath());
250            return null;
251        }
252
253        // Create new container
254        final String newCachePath;
255        if ((newCachePath = PackageHelper.createSdDir(sizeMb, newCid, key, Process.myUid())) == null) {
256            Slog.e(TAG, "Failed to create container " + newCid);
257            return null;
258        }
259
260        if (localLOGV) {
261            Slog.i(TAG, "Created container for " + newCid + " at path : " + newCachePath);
262        }
263
264        final File resFile = new File(newCachePath, resFileName);
265        if (FileUtils.copyFile(new File(codePath), resFile)) {
266            if (localLOGV) {
267                Slog.i(TAG, "Copied " + codePath + " to " + resFile);
268            }
269        } else {
270            Slog.e(TAG, "Failed to copy " + codePath + " to " + resFile);
271            // Clean up container
272            PackageHelper.destroySdDir(newCid);
273            return null;
274        }
275
276        final File sharedLibraryDir = new File(newCachePath, LIB_DIR_NAME);
277        if (sharedLibraryDir.mkdir()) {
278            int ret = NativeLibraryHelper.copyNativeBinariesIfNeededLI(codeFile, sharedLibraryDir);
279            if (ret != PackageManager.INSTALL_SUCCEEDED) {
280                Slog.e(TAG, "Could not copy native libraries to " + sharedLibraryDir.getPath());
281                PackageHelper.destroySdDir(newCid);
282                return null;
283            }
284        } else {
285            Slog.e(TAG, "Could not create native lib directory: " + sharedLibraryDir.getPath());
286            PackageHelper.destroySdDir(newCid);
287            return null;
288        }
289
290        if (!PackageHelper.finalizeSdDir(newCid)) {
291            Slog.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
292            // Clean up container
293            PackageHelper.destroySdDir(newCid);
294            return null;
295        }
296
297        if (localLOGV) {
298            Slog.i(TAG, "Finalized container " + newCid);
299        }
300
301        if (PackageHelper.isContainerMounted(newCid)) {
302            if (localLOGV) {
303                Slog.i(TAG, "Unmounting " + newCid + " at path " + newCachePath);
304            }
305
306            // Force a gc to avoid being killed.
307            Runtime.getRuntime().gc();
308            PackageHelper.unMountSdDir(newCid);
309        } else {
310            if (localLOGV) {
311                Slog.i(TAG, "Container " + newCid + " not mounted");
312            }
313        }
314
315        return newCachePath;
316    }
317
318    private static boolean copyToFile(InputStream inputStream, FileOutputStream out) {
319        try {
320            byte[] buffer = new byte[4096];
321            int bytesRead;
322            while ((bytesRead = inputStream.read(buffer)) >= 0) {
323                out.write(buffer, 0, bytesRead);
324            }
325            return true;
326        } catch (IOException e) {
327            Slog.i(TAG, "Exception : " + e + " when copying file");
328            return false;
329        }
330    }
331
332    private static boolean copyToFile(File srcFile, FileOutputStream out) {
333        InputStream inputStream = null;
334        try {
335            inputStream = new FileInputStream(srcFile);
336            return copyToFile(inputStream, out);
337        } catch (IOException e) {
338            return false;
339        } finally {
340            try { if (inputStream != null) inputStream.close(); } catch (IOException e) {}
341        }
342    }
343
344    private boolean copyFile(Uri pPackageURI, FileOutputStream outStream) {
345        String scheme = pPackageURI.getScheme();
346        if (scheme == null || scheme.equals("file")) {
347            final File srcPackageFile = new File(pPackageURI.getPath());
348            // We copy the source package file to a temp file and then rename it to the
349            // destination file in order to eliminate a window where the package directory
350            // scanner notices the new package file but it's not completely copied yet.
351            if (!copyToFile(srcPackageFile, outStream)) {
352                Slog.e(TAG, "Couldn't copy file: " + srcPackageFile);
353                return false;
354            }
355        } else if (scheme.equals("content")) {
356            ParcelFileDescriptor fd = null;
357            try {
358                fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
359            } catch (FileNotFoundException e) {
360                Slog.e(TAG,
361                        "Couldn't open file descriptor from download service. Failed with exception "
362                                + e);
363                return false;
364            }
365            if (fd == null) {
366                Slog.e(TAG, "Couldn't open file descriptor from download service (null).");
367                return false;
368            } else {
369                if (localLOGV) {
370                    Slog.i(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
377                // cop
378                if (!copyToFile(dlStream, outStream)) {
379                    Slog.e(TAG, "Couldn't copy " + pPackageURI + " to temp file.");
380                    return false;
381                }
382            }
383        } else {
384            Slog.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
385            return false;
386        }
387        return true;
388    }
389
390    private static final int PREFER_INTERNAL = 1;
391    private static final int PREFER_EXTERNAL = 2;
392
393    private int recommendAppInstallLocation(int installLocation, String archiveFilePath, int flags,
394            long threshold) {
395        int prefer;
396        boolean checkBoth = false;
397
398        check_inner : {
399            /*
400             * Explicit install flags should override the manifest settings.
401             */
402            if ((flags & PackageManager.INSTALL_FORWARD_LOCK) != 0) {
403                /*
404                 * Forward-locked applications cannot be installed on SD card,
405                 * so only allow checking internal storage.
406                 */
407                prefer = PREFER_INTERNAL;
408                break check_inner;
409            } else if ((flags & PackageManager.INSTALL_INTERNAL) != 0) {
410                prefer = PREFER_INTERNAL;
411                break check_inner;
412            } else if ((flags & PackageManager.INSTALL_EXTERNAL) != 0) {
413                prefer = PREFER_EXTERNAL;
414                break check_inner;
415            }
416
417            /* No install flags. Check for manifest option. */
418            if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
419                prefer = PREFER_INTERNAL;
420                break check_inner;
421            } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
422                prefer = PREFER_EXTERNAL;
423                checkBoth = true;
424                break check_inner;
425            } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
426                // We default to preferring internal storage.
427                prefer = PREFER_INTERNAL;
428                checkBoth = true;
429                break check_inner;
430            }
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                prefer = PREFER_INTERNAL;
439                break check_inner;
440            } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
441                prefer = PREFER_EXTERNAL;
442                break check_inner;
443            }
444
445            /*
446             * Fall back to default policy of internal-only if nothing else is
447             * specified.
448             */
449            prefer = PREFER_INTERNAL;
450        }
451
452        final boolean emulated = Environment.isExternalStorageEmulated();
453
454        final File apkFile = new File(archiveFilePath);
455
456        boolean fitsOnInternal = false;
457        if (checkBoth || prefer == PREFER_INTERNAL) {
458            try {
459                fitsOnInternal = isUnderInternalThreshold(apkFile, threshold);
460            } catch (FileNotFoundException e) {
461                return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
462            }
463        }
464
465        boolean fitsOnSd = false;
466        if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {
467            try {
468                fitsOnSd = isUnderExternalThreshold(apkFile);
469            } catch (FileNotFoundException e) {
470                return PackageHelper.RECOMMEND_FAILED_INVALID_URI;
471            }
472        }
473
474        if (prefer == PREFER_INTERNAL) {
475            if (fitsOnInternal) {
476                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
477            }
478        } else if (!emulated && prefer == PREFER_EXTERNAL) {
479            if (fitsOnSd) {
480                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
481            }
482        }
483
484        if (checkBoth) {
485            if (fitsOnInternal) {
486                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
487            } else if (!emulated && fitsOnSd) {
488                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
489            }
490        }
491
492        /*
493         * If they requested to be on the external media by default, return that
494         * the media was unavailable. Otherwise, indicate there was insufficient
495         * storage space available.
496         */
497        if (!emulated && (checkBoth || prefer == PREFER_EXTERNAL)
498                && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
499            return PackageHelper.RECOMMEND_MEDIA_UNAVAILABLE;
500        } else {
501            return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
502        }
503    }
504
505    /**
506     * Measure a file to see if it fits within the free space threshold.
507     *
508     * @param apkFile file to check
509     * @param threshold byte threshold to compare against
510     * @return true if file fits under threshold
511     * @throws FileNotFoundException when APK does not exist
512     */
513    private boolean isUnderInternalThreshold(File apkFile, long threshold)
514            throws FileNotFoundException {
515        final long size = apkFile.length();
516        if (size == 0 && !apkFile.exists()) {
517            throw new FileNotFoundException();
518        }
519
520        final StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
521        final long availInternalSize = (long) internalStats.getAvailableBlocks()
522                * (long) internalStats.getBlockSize();
523
524        return (availInternalSize - size) > threshold;
525    }
526
527
528    /**
529     * Measure a file to see if it fits in the external free space.
530     *
531     * @param apkFile file to check
532     * @return true if file fits
533     * @throws IOException when file does not exist
534     */
535    private boolean isUnderExternalThreshold(File apkFile) throws FileNotFoundException {
536        if (Environment.isExternalStorageEmulated()) {
537            return false;
538        }
539
540        final int sizeMb = calculateContainerSize(apkFile);
541
542        final int availSdMb;
543        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
544            StatFs sdStats = new StatFs(Environment.getExternalStorageDirectory().getPath());
545            long availSdSize = (long) (sdStats.getAvailableBlocks() * sdStats.getBlockSize());
546            availSdMb = (int) (availSdSize >> 20);
547        } else {
548            availSdMb = -1;
549        }
550
551        return availSdMb > sizeMb;
552    }
553
554    /**
555     * Calculate the container size for an APK. Takes into account the
556     *
557     * @param apkFile file from which to calculate size
558     * @return size in megabytes (2^20 bytes)
559     * @throws FileNotFoundException when file does not exist
560     */
561    private int calculateContainerSize(File apkFile) throws FileNotFoundException {
562        // Calculate size of container needed to hold base APK.
563        long sizeBytes = apkFile.length();
564        if (sizeBytes == 0 && !apkFile.exists()) {
565            throw new FileNotFoundException();
566        }
567
568        // Check all the native files that need to be copied and add that to the
569        // container size.
570        sizeBytes += NativeLibraryHelper.sumNativeBinariesLI(apkFile);
571
572        int sizeMb = (int) (sizeBytes >> 20);
573        if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) {
574            sizeMb++;
575        }
576
577        /*
578         * Add buffer size because we don't have a good way to determine the
579         * real FAT size. Your FAT size varies with how many directory entries
580         * you need, how big the whole filesystem is, and other such headaches.
581         */
582        sizeMb++;
583
584        return sizeMb;
585    }
586}
587