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