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