DefaultContainerService.java revision 15a4d2ffd04dc6c70f2cd17dae12ac6bc14c69ab
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.PackageHelper;
21import android.content.Intent;
22import android.content.pm.IPackageManager;
23import android.content.pm.PackageInfo;
24import android.content.pm.PackageInfoLite;
25import android.content.pm.PackageManager;
26import android.content.pm.PackageParser;
27import android.content.pm.PackageParser.Package;
28import android.net.Uri;
29import android.os.Environment;
30import android.os.IBinder;
31import android.os.ParcelFileDescriptor;
32import android.os.Process;
33import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.os.StatFs;
36import android.app.IntentService;
37import android.util.DisplayMetrics;
38import android.util.Log;
39
40import java.io.File;
41import java.io.FileInputStream;
42import java.io.FileNotFoundException;
43import java.io.FileOutputStream;
44import java.io.IOException;
45import java.io.InputStream;
46
47import android.os.FileUtils;
48import android.os.storage.IMountService;
49import android.provider.Settings;
50
51/*
52 * This service copies a downloaded apk to a file passed in as
53 * a ParcelFileDescriptor or to a newly created container specified
54 * by parameters. The DownloadManager gives access to this process
55 * based on its uid. This process also needs the ACCESS_DOWNLOAD_MANAGER
56 * permission to access apks downloaded via the download manager.
57 */
58public class DefaultContainerService extends IntentService {
59    private static final String TAG = "DefContainer";
60    private static final boolean localLOGV = true;
61
62    private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
63        /*
64         * Creates a new container and copies resource there.
65         * @param paackageURI the uri of resource to be copied. Can be either
66         * a content uri or a file uri
67         * @param cid the id of the secure container that should
68         * be used for creating a secure container into which the resource
69         * will be copied.
70         * @param key Refers to key used for encrypting the secure container
71         * @param resFileName Name of the target resource file(relative to newly
72         * created secure container)
73         * @return Returns the new cache path where the resource has been copied into
74         *
75         */
76        public String copyResourceToContainer(final Uri packageURI,
77                final String cid,
78                final String key, final String resFileName) {
79            if (packageURI == null || cid == null) {
80                return null;
81            }
82            return copyResourceInner(packageURI, cid, key, resFileName);
83        }
84
85        /*
86         * Copy specified resource to output stream
87         * @param packageURI the uri of resource to be copied. Should be a
88         * file uri
89         * @param outStream Remote file descriptor to be used for copying
90         * @return Returns true if copy succeded or false otherwise.
91         */
92        public boolean copyResource(final Uri packageURI,
93                ParcelFileDescriptor outStream) {
94            if (packageURI == null ||  outStream == null) {
95                return false;
96            }
97            ParcelFileDescriptor.AutoCloseOutputStream
98            autoOut = new ParcelFileDescriptor.AutoCloseOutputStream(outStream);
99            return copyFile(packageURI, autoOut);
100        }
101
102        /*
103         * Determine the recommended install location for package
104         * specified by file uri location.
105         * @param fileUri the uri of resource to be copied. Should be a
106         * file uri
107         * @return Returns PackageInfoLite object containing
108         * the package info and recommended app location.
109         */
110        public PackageInfoLite getMinimalPackageInfo(final Uri fileUri) {
111            PackageInfoLite ret = new PackageInfoLite();
112            if (fileUri == null) {
113                Log.i(TAG, "Invalid package uri " + fileUri);
114                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
115                return ret;
116            }
117            String scheme = fileUri.getScheme();
118            if (scheme != null && !scheme.equals("file")) {
119                Log.w(TAG, "Falling back to installing on internal storage only");
120                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_INSTALL_INTERNAL;
121                return ret;
122            }
123            String archiveFilePath = fileUri.getPath();
124            PackageParser packageParser = new PackageParser(archiveFilePath);
125            File sourceFile = new File(archiveFilePath);
126            DisplayMetrics metrics = new DisplayMetrics();
127            metrics.setToDefaults();
128            PackageParser.PackageLite pkg = packageParser.parsePackageLite(
129                    archiveFilePath, 0);
130            ret.packageName = pkg.packageName;
131            ret.installLocation = pkg.installLocation;
132            // Nuke the parser reference right away and force a gc
133            Runtime.getRuntime().gc();
134            packageParser = null;
135            if (pkg == null) {
136                Log.w(TAG, "Failed to parse package");
137                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
138                return ret;
139            }
140            ret.packageName = pkg.packageName;
141            int loc = recommendAppInstallLocation(pkg.installLocation, archiveFilePath);
142            if (loc == PackageManager.INSTALL_EXTERNAL) {
143                ret.recommendedInstallLocation =  PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
144            } else if (loc == ERR_LOC) {
145                Log.i(TAG, "Failed to install insufficient storage");
146                ret.recommendedInstallLocation =  PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
147            } else {
148                // Implies install on internal storage.
149                ret.recommendedInstallLocation =  PackageHelper.RECOMMEND_INSTALL_INTERNAL;
150            }
151            return ret;
152        }
153    };
154
155    public DefaultContainerService() {
156        super("DefaultContainerService");
157        setIntentRedelivery(true);
158    }
159
160    @Override
161    protected void onHandleIntent(Intent intent) {
162        if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
163            IPackageManager pm = IPackageManager.Stub.asInterface(
164                    ServiceManager.getService("package"));
165            String pkg = null;
166            try {
167                while ((pkg=pm.nextPackageToClean(pkg)) != null) {
168                    eraseFiles(Environment.getExternalStorageAppDataDirectory(pkg));
169                    eraseFiles(Environment.getExternalStorageAppMediaDirectory(pkg));
170                }
171            } catch (RemoteException e) {
172            }
173        }
174    }
175
176    void eraseFiles(File path) {
177        if (path.isDirectory()) {
178            String[] files = path.list();
179            if (files != null) {
180                for (String file : files) {
181                    eraseFiles(new File(path, file));
182                }
183            }
184        }
185        path.delete();
186    }
187
188    public IBinder onBind(Intent intent) {
189        return mBinder;
190    }
191
192    private String copyResourceInner(Uri packageURI, String newCid, String key, String resFileName) {
193        // Create new container at newCachePath
194        String codePath = packageURI.getPath();
195        File codeFile = new File(codePath);
196        String newCachePath = null;
197        // Create new container
198        if ((newCachePath = PackageHelper.createSdDir(codeFile,
199                newCid, key, Process.myUid())) == null) {
200            Log.e(TAG, "Failed to create container " + newCid);
201            return null;
202        }
203        if (localLOGV) Log.i(TAG, "Created container for " + newCid
204                + " at path : " + newCachePath);
205        File resFile = new File(newCachePath, resFileName);
206        if (!FileUtils.copyFile(new File(codePath), resFile)) {
207            Log.e(TAG, "Failed to copy " + codePath + " to " + resFile);
208            // Clean up container
209            PackageHelper.destroySdDir(newCid);
210            return null;
211        }
212        if (localLOGV) Log.i(TAG, "Copied " + codePath + " to " + resFile);
213        if (!PackageHelper.finalizeSdDir(newCid)) {
214            Log.e(TAG, "Failed to finalize " + newCid + " at path " + newCachePath);
215            // Clean up container
216            PackageHelper.destroySdDir(newCid);
217        }
218        if (localLOGV) Log.i(TAG, "Finalized container " + newCid);
219        if (PackageHelper.isContainerMounted(newCid)) {
220            if (localLOGV) Log.i(TAG, "Unmounting " + newCid +
221                    " at path " + newCachePath);
222            // Force a gc to avoid being killed.
223            Runtime.getRuntime().gc();
224            PackageHelper.unMountSdDir(newCid);
225        } else {
226            if (localLOGV) Log.i(TAG, "Container " + newCid + " not mounted");
227        }
228        return newCachePath;
229    }
230
231    public static boolean copyToFile(InputStream inputStream, FileOutputStream out) {
232        try {
233            byte[] buffer = new byte[4096];
234            int bytesRead;
235            while ((bytesRead = inputStream.read(buffer)) >= 0) {
236                out.write(buffer, 0, bytesRead);
237            }
238            return true;
239        } catch (IOException e) {
240            Log.i(TAG, "Exception : " + e + " when copying file");
241            return false;
242        }
243    }
244
245    public static boolean copyToFile(File srcFile, FileOutputStream out) {
246        InputStream inputStream = null;
247        try {
248            inputStream = new FileInputStream(srcFile);
249            return copyToFile(inputStream, out);
250        } catch (IOException e) {
251            return false;
252        } finally {
253            try { if (inputStream != null) inputStream.close(); } catch (IOException e) {}
254        }
255    }
256
257    private  boolean copyFile(Uri pPackageURI, FileOutputStream outStream) {
258        String scheme = pPackageURI.getScheme();
259        if (scheme == null || scheme.equals("file")) {
260            final File srcPackageFile = new File(pPackageURI.getPath());
261            // We copy the source package file to a temp file and then rename it to the
262            // destination file in order to eliminate a window where the package directory
263            // scanner notices the new package file but it's not completely copied yet.
264            if (!copyToFile(srcPackageFile, outStream)) {
265                Log.e(TAG, "Couldn't copy file: " + srcPackageFile);
266                return false;
267            }
268        } else if (scheme.equals("content")) {
269            ParcelFileDescriptor fd = null;
270            try {
271                fd = getContentResolver().openFileDescriptor(pPackageURI, "r");
272            } catch (FileNotFoundException e) {
273                Log.e(TAG, "Couldn't open file descriptor from download service. Failed with exception " + e);
274                return false;
275            }
276            if (fd == null) {
277                Log.e(TAG, "Couldn't open file descriptor from download service (null).");
278                return false;
279            } else {
280                if (localLOGV) {
281                    Log.v(TAG, "Opened file descriptor from download service.");
282                }
283                ParcelFileDescriptor.AutoCloseInputStream
284                dlStream = new ParcelFileDescriptor.AutoCloseInputStream(fd);
285                // We copy the source package file to a temp file and then rename it to the
286                // destination file in order to eliminate a window where the package directory
287                // scanner notices the new package file but it's not completely copied yet.
288                if (!copyToFile(dlStream, outStream)) {
289                    Log.e(TAG, "Couldn't copy " + pPackageURI + " to temp file.");
290                    return false;
291                }
292            }
293        } else {
294            Log.e(TAG, "Package URI is not 'file:' or 'content:' - " + pPackageURI);
295            return false;
296        }
297        return true;
298    }
299
300    // Constants related to app heuristics
301    // No-installation limit for internal flash: 10% or less space available
302    private static final double LOW_NAND_FLASH_TRESHOLD = 0.1;
303
304    // SD-to-internal app size threshold: currently set to 1 MB
305    private static final long INSTALL_ON_SD_THRESHOLD = (1024 * 1024);
306    private static final int ERR_LOC = -1;
307
308    private int recommendAppInstallLocation(int installLocation,
309            String archiveFilePath) {
310        // Initial implementation:
311        // Package size = code size + cache size + data size
312        // If code size > 1 MB, install on SD card.
313        // Else install on internal NAND flash, unless space on NAND is less than 10%
314        String status = Environment.getExternalStorageState();
315        long availSDSize = -1;
316        if (status.equals(Environment.MEDIA_MOUNTED)) {
317            StatFs sdStats = new StatFs(
318                    Environment.getExternalStorageDirectory().getPath());
319            availSDSize = (long)sdStats.getAvailableBlocks() *
320                    (long)sdStats.getBlockSize();
321        }
322        StatFs internalStats = new StatFs(Environment.getDataDirectory().getPath());
323        long totalInternalSize = (long)internalStats.getBlockCount() *
324                (long)internalStats.getBlockSize();
325        long availInternalSize = (long)internalStats.getAvailableBlocks() *
326                (long)internalStats.getBlockSize();
327
328        double pctNandFree = (double)availInternalSize / (double)totalInternalSize;
329
330        File apkFile = new File(archiveFilePath);
331        long pkgLen = apkFile.length();
332
333        boolean auto = true;
334        // To make final copy
335        long reqInstallSize = pkgLen;
336        // For dex files. Just ignore and fail when extracting. Max limit of 2Gig for now.
337        long reqInternalSize = 0;
338        boolean intThresholdOk = (pctNandFree >= LOW_NAND_FLASH_TRESHOLD);
339        boolean intAvailOk = ((reqInstallSize + reqInternalSize) < availInternalSize);
340        boolean fitsOnSd = (reqInstallSize < availSDSize) && intThresholdOk &&
341                (reqInternalSize < availInternalSize);
342        boolean fitsOnInt = intThresholdOk && intAvailOk;
343
344        // Consider application flags preferences as well...
345        boolean installOnlyOnSd = (installLocation ==
346                PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL);
347        boolean installOnlyInternal = (installLocation ==
348                PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY);
349        if (installOnlyInternal) {
350            // If set explicitly in manifest,
351            // let that override everything else
352            auto = false;
353        } else if (installOnlyOnSd){
354            // Check if this can be accommodated on the sdcard
355            if (fitsOnSd) {
356                auto = false;
357            }
358        } else {
359            // Check if user option is enabled
360            boolean setInstallLoc = Settings.System.getInt(getApplicationContext()
361                    .getContentResolver(),
362                    Settings.System.SET_INSTALL_LOCATION, 0) != 0;
363            if (setInstallLoc) {
364                // Pick user preference
365                int installPreference = Settings.System.getInt(getApplicationContext()
366                        .getContentResolver(),
367                        Settings.System.DEFAULT_INSTALL_LOCATION,
368                        PackageHelper.APP_INSTALL_AUTO);
369                if (installPreference == PackageHelper.APP_INSTALL_INTERNAL) {
370                    installOnlyInternal = true;
371                    auto = false;
372                } else if (installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {
373                    installOnlyOnSd = true;
374                    auto = false;
375                }
376            }
377        }
378        if (!auto) {
379            if (installOnlyOnSd) {
380                return fitsOnSd ? PackageManager.INSTALL_EXTERNAL : ERR_LOC;
381            } else if (installOnlyInternal){
382                // Check on internal flash
383                return fitsOnInt ? 0 : ERR_LOC;
384            }
385        }
386        // Try to install internally
387        if (fitsOnInt) {
388            return 0;
389        }
390        // Try the sdcard now.
391        if (fitsOnSd) {
392            return PackageManager.INSTALL_EXTERNAL;
393        }
394        // Return error code
395        return ERR_LOC;
396    }
397}
398