PackageHelper.java revision f39ca8f5e5f43f130b4590921087a63f88bb8286
1/*
2 * Copyright (C) 2009 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.internal.content;
18
19import static android.net.TrafficStats.MB_IN_BYTES;
20
21import android.content.Context;
22import android.content.pm.ApplicationInfo;
23import android.content.pm.PackageInfo;
24import android.content.pm.PackageManager;
25import android.content.pm.PackageManager.NameNotFoundException;
26import android.content.pm.PackageParser.PackageLite;
27import android.os.Environment;
28import android.os.FileUtils;
29import android.os.IBinder;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.os.storage.IMountService;
33import android.os.storage.StorageManager;
34import android.os.storage.StorageResultCode;
35import android.os.storage.StorageVolume;
36import android.os.storage.VolumeInfo;
37import android.provider.Settings;
38import android.util.ArraySet;
39import android.util.Log;
40import android.util.Slog;
41
42import libcore.io.IoUtils;
43
44import java.io.File;
45import java.io.FileOutputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.util.Collections;
49import java.util.zip.ZipEntry;
50import java.util.zip.ZipFile;
51import java.util.zip.ZipOutputStream;
52
53/**
54 * Constants used internally between the PackageManager
55 * and media container service transports.
56 * Some utility methods to invoke MountService api.
57 */
58public class PackageHelper {
59    public static final int RECOMMEND_INSTALL_INTERNAL = 1;
60    public static final int RECOMMEND_INSTALL_EXTERNAL = 2;
61    public static final int RECOMMEND_FAILED_INSUFFICIENT_STORAGE = -1;
62    public static final int RECOMMEND_FAILED_INVALID_APK = -2;
63    public static final int RECOMMEND_FAILED_INVALID_LOCATION = -3;
64    public static final int RECOMMEND_FAILED_ALREADY_EXISTS = -4;
65    public static final int RECOMMEND_MEDIA_UNAVAILABLE = -5;
66    public static final int RECOMMEND_FAILED_INVALID_URI = -6;
67    public static final int RECOMMEND_FAILED_VERSION_DOWNGRADE = -7;
68
69    private static final boolean localLOGV = false;
70    private static final String TAG = "PackageHelper";
71    // App installation location settings values
72    public static final int APP_INSTALL_AUTO = 0;
73    public static final int APP_INSTALL_INTERNAL = 1;
74    public static final int APP_INSTALL_EXTERNAL = 2;
75
76    public static IMountService getMountService() throws RemoteException {
77        IBinder service = ServiceManager.getService("mount");
78        if (service != null) {
79            return IMountService.Stub.asInterface(service);
80        } else {
81            Log.e(TAG, "Can't get mount service");
82            throw new RemoteException("Could not contact mount service");
83        }
84    }
85
86    public static String createSdDir(long sizeBytes, String cid, String sdEncKey, int uid,
87            boolean isExternal) {
88        // Round up to nearest MB, plus another MB for filesystem overhead
89        final int sizeMb = (int) ((sizeBytes + MB_IN_BYTES) / MB_IN_BYTES) + 1;
90        try {
91            IMountService mountService = getMountService();
92
93            if (localLOGV)
94                Log.i(TAG, "Size of container " + sizeMb + " MB");
95
96            int rc = mountService.createSecureContainer(cid, sizeMb, "ext4", sdEncKey, uid,
97                    isExternal);
98            if (rc != StorageResultCode.OperationSucceeded) {
99                Log.e(TAG, "Failed to create secure container " + cid);
100                return null;
101            }
102            String cachePath = mountService.getSecureContainerPath(cid);
103            if (localLOGV) Log.i(TAG, "Created secure container " + cid +
104                    " at " + cachePath);
105                return cachePath;
106        } catch (RemoteException e) {
107            Log.e(TAG, "MountService running?");
108        }
109        return null;
110    }
111
112    public static boolean resizeSdDir(long sizeBytes, String cid, String sdEncKey) {
113        // Round up to nearest MB, plus another MB for filesystem overhead
114        final int sizeMb = (int) ((sizeBytes + MB_IN_BYTES) / MB_IN_BYTES) + 1;
115        try {
116            IMountService mountService = getMountService();
117            int rc = mountService.resizeSecureContainer(cid, sizeMb, sdEncKey);
118            if (rc == StorageResultCode.OperationSucceeded) {
119                return true;
120            }
121        } catch (RemoteException e) {
122            Log.e(TAG, "MountService running?");
123        }
124        Log.e(TAG, "Failed to create secure container " + cid);
125        return false;
126    }
127
128    public static String mountSdDir(String cid, String key, int ownerUid) {
129        return mountSdDir(cid, key, ownerUid, true);
130    }
131
132    public static String mountSdDir(String cid, String key, int ownerUid, boolean readOnly) {
133        try {
134            int rc = getMountService().mountSecureContainer(cid, key, ownerUid, readOnly);
135            if (rc != StorageResultCode.OperationSucceeded) {
136                Log.i(TAG, "Failed to mount container " + cid + " rc : " + rc);
137                return null;
138            }
139            return getMountService().getSecureContainerPath(cid);
140        } catch (RemoteException e) {
141            Log.e(TAG, "MountService running?");
142        }
143        return null;
144    }
145
146   public static boolean unMountSdDir(String cid) {
147    try {
148        int rc = getMountService().unmountSecureContainer(cid, true);
149        if (rc != StorageResultCode.OperationSucceeded) {
150            Log.e(TAG, "Failed to unmount " + cid + " with rc " + rc);
151            return false;
152        }
153        return true;
154    } catch (RemoteException e) {
155        Log.e(TAG, "MountService running?");
156    }
157        return false;
158   }
159
160   public static boolean renameSdDir(String oldId, String newId) {
161       try {
162           int rc = getMountService().renameSecureContainer(oldId, newId);
163           if (rc != StorageResultCode.OperationSucceeded) {
164               Log.e(TAG, "Failed to rename " + oldId + " to " +
165                       newId + "with rc " + rc);
166               return false;
167           }
168           return true;
169       } catch (RemoteException e) {
170           Log.i(TAG, "Failed ot rename  " + oldId + " to " + newId +
171                   " with exception : " + e);
172       }
173       return false;
174   }
175
176   public static String getSdDir(String cid) {
177       try {
178            return getMountService().getSecureContainerPath(cid);
179        } catch (RemoteException e) {
180            Log.e(TAG, "Failed to get container path for " + cid +
181                " with exception " + e);
182        }
183        return null;
184   }
185
186   public static String getSdFilesystem(String cid) {
187       try {
188            return getMountService().getSecureContainerFilesystemPath(cid);
189        } catch (RemoteException e) {
190            Log.e(TAG, "Failed to get container path for " + cid +
191                " with exception " + e);
192        }
193        return null;
194   }
195
196    public static boolean finalizeSdDir(String cid) {
197        try {
198            int rc = getMountService().finalizeSecureContainer(cid);
199            if (rc != StorageResultCode.OperationSucceeded) {
200                Log.i(TAG, "Failed to finalize container " + cid);
201                return false;
202            }
203            return true;
204        } catch (RemoteException e) {
205            Log.e(TAG, "Failed to finalize container " + cid +
206                    " with exception " + e);
207        }
208        return false;
209    }
210
211    public static boolean destroySdDir(String cid) {
212        try {
213            if (localLOGV) Log.i(TAG, "Forcibly destroying container " + cid);
214            int rc = getMountService().destroySecureContainer(cid, true);
215            if (rc != StorageResultCode.OperationSucceeded) {
216                Log.i(TAG, "Failed to destroy container " + cid);
217                return false;
218            }
219            return true;
220        } catch (RemoteException e) {
221            Log.e(TAG, "Failed to destroy container " + cid +
222                    " with exception " + e);
223        }
224        return false;
225    }
226
227    public static String[] getSecureContainerList() {
228        try {
229            return getMountService().getSecureContainerList();
230        } catch (RemoteException e) {
231            Log.e(TAG, "Failed to get secure container list with exception" +
232                    e);
233        }
234        return null;
235    }
236
237   public static boolean isContainerMounted(String cid) {
238       try {
239           return getMountService().isSecureContainerMounted(cid);
240       } catch (RemoteException e) {
241           Log.e(TAG, "Failed to find out if container " + cid + " mounted");
242       }
243       return false;
244   }
245
246    /**
247     * Extract public files for the single given APK.
248     */
249    public static long extractPublicFiles(File apkFile, File publicZipFile)
250            throws IOException {
251        final FileOutputStream fstr;
252        final ZipOutputStream publicZipOutStream;
253
254        if (publicZipFile == null) {
255            fstr = null;
256            publicZipOutStream = null;
257        } else {
258            fstr = new FileOutputStream(publicZipFile);
259            publicZipOutStream = new ZipOutputStream(fstr);
260            Log.d(TAG, "Extracting " + apkFile + " to " + publicZipFile);
261        }
262
263        long size = 0L;
264
265        try {
266            final ZipFile privateZip = new ZipFile(apkFile.getAbsolutePath());
267            try {
268                // Copy manifest, resources.arsc and res directory to public zip
269                for (final ZipEntry zipEntry : Collections.list(privateZip.entries())) {
270                    final String zipEntryName = zipEntry.getName();
271                    if ("AndroidManifest.xml".equals(zipEntryName)
272                            || "resources.arsc".equals(zipEntryName)
273                            || zipEntryName.startsWith("res/")) {
274                        size += zipEntry.getSize();
275                        if (publicZipFile != null) {
276                            copyZipEntry(zipEntry, privateZip, publicZipOutStream);
277                        }
278                    }
279                }
280            } finally {
281                try { privateZip.close(); } catch (IOException e) {}
282            }
283
284            if (publicZipFile != null) {
285                publicZipOutStream.finish();
286                publicZipOutStream.flush();
287                FileUtils.sync(fstr);
288                publicZipOutStream.close();
289                FileUtils.setPermissions(publicZipFile.getAbsolutePath(), FileUtils.S_IRUSR
290                        | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IROTH, -1, -1);
291            }
292        } finally {
293            IoUtils.closeQuietly(publicZipOutStream);
294        }
295
296        return size;
297    }
298
299    private static void copyZipEntry(ZipEntry zipEntry, ZipFile inZipFile,
300            ZipOutputStream outZipStream) throws IOException {
301        byte[] buffer = new byte[4096];
302        int num;
303
304        ZipEntry newEntry;
305        if (zipEntry.getMethod() == ZipEntry.STORED) {
306            // Preserve the STORED method of the input entry.
307            newEntry = new ZipEntry(zipEntry);
308        } else {
309            // Create a new entry so that the compressed len is recomputed.
310            newEntry = new ZipEntry(zipEntry.getName());
311        }
312        outZipStream.putNextEntry(newEntry);
313
314        final InputStream data = inZipFile.getInputStream(zipEntry);
315        try {
316            while ((num = data.read(buffer)) > 0) {
317                outZipStream.write(buffer, 0, num);
318            }
319            outZipStream.flush();
320        } finally {
321            IoUtils.closeQuietly(data);
322        }
323    }
324
325    public static boolean fixSdPermissions(String cid, int gid, String filename) {
326        try {
327            int rc = getMountService().fixPermissionsSecureContainer(cid, gid, filename);
328            if (rc != StorageResultCode.OperationSucceeded) {
329                Log.i(TAG, "Failed to fixperms container " + cid);
330                return false;
331            }
332            return true;
333        } catch (RemoteException e) {
334            Log.e(TAG, "Failed to fixperms container " + cid + " with exception " + e);
335        }
336        return false;
337    }
338
339    /**
340     * Given a requested {@link PackageInfo#installLocation} and calculated
341     * install size, pick the actual volume to install the app. Only considers
342     * internal and private volumes, and prefers to keep an existing package on
343     * its current volume.
344     *
345     * @return the {@link VolumeInfo#fsUuid} to install onto, or {@code null}
346     *         for internal storage.
347     */
348    public static String resolveInstallVolume(Context context, String packageName,
349            int installLocation, long sizeBytes) throws IOException {
350        final boolean forceAllowOnExternal = Settings.Global.getInt(
351                context.getContentResolver(), Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0;
352        // TODO: handle existing apps installed in ASEC; currently assumes
353        // they'll end up back on internal storage
354        ApplicationInfo existingInfo = null;
355        try {
356            existingInfo = context.getPackageManager().getApplicationInfo(packageName,
357                    PackageManager.GET_UNINSTALLED_PACKAGES);
358        } catch (NameNotFoundException ignored) {
359        }
360
361        final StorageManager storageManager = context.getSystemService(StorageManager.class);
362        final boolean fitsOnInternal = fitsOnInternal(context, sizeBytes);
363
364        final ArraySet<String> allCandidates = new ArraySet<>();
365        VolumeInfo bestCandidate = null;
366        long bestCandidateAvailBytes = Long.MIN_VALUE;
367        for (VolumeInfo vol : storageManager.getVolumes()) {
368            if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
369                final long availBytes = storageManager.getStorageBytesUntilLow(new File(vol.path));
370                if (availBytes >= sizeBytes) {
371                    allCandidates.add(vol.fsUuid);
372                }
373                if (availBytes >= bestCandidateAvailBytes) {
374                    bestCandidate = vol;
375                    bestCandidateAvailBytes = availBytes;
376                }
377            }
378        }
379
380        // System apps always forced to internal storage
381        if (existingInfo != null && existingInfo.isSystemApp()) {
382            installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY;
383        }
384
385        // If app expresses strong desire for internal space, honor it
386        if (!forceAllowOnExternal
387                && installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
388            if (fitsOnInternal) {
389                return null;
390            } else {
391                throw new IOException("Requested internal only, but not enough space");
392            }
393        }
394
395        // If app already exists somewhere, prefer to stay on that volume
396        if (existingInfo != null) {
397            if (existingInfo.volumeUuid == null && fitsOnInternal) {
398                return null;
399            }
400            if (allCandidates.contains(existingInfo.volumeUuid)) {
401                return existingInfo.volumeUuid;
402            }
403        }
404
405        // We're left with either preferring external or auto, so just pick
406        // volume with most space
407        if (bestCandidate != null) {
408            return bestCandidate.fsUuid;
409        } else if (fitsOnInternal) {
410            return null;
411        } else {
412            throw new IOException("No special requests, but no room anywhere");
413        }
414    }
415
416    public static boolean fitsOnInternal(Context context, long sizeBytes) {
417        final StorageManager storage = context.getSystemService(StorageManager.class);
418        final File target = Environment.getDataDirectory();
419        return (sizeBytes <= storage.getStorageBytesUntilLow(target));
420    }
421
422    public static boolean fitsOnExternal(Context context, long sizeBytes) {
423        final StorageManager storage = context.getSystemService(StorageManager.class);
424        final StorageVolume primary = storage.getPrimaryVolume();
425        return (sizeBytes > 0) && !primary.isEmulated()
426                && Environment.MEDIA_MOUNTED.equals(primary.getState())
427                && sizeBytes <= storage.getStorageBytesUntilLow(primary.getPathFile());
428    }
429
430    /**
431     * Given a requested {@link PackageInfo#installLocation} and calculated
432     * install size, pick the actual location to install the app.
433     */
434    public static int resolveInstallLocation(Context context, String packageName,
435            int installLocation, long sizeBytes, int installFlags) {
436        ApplicationInfo existingInfo = null;
437        try {
438            existingInfo = context.getPackageManager().getApplicationInfo(packageName,
439                    PackageManager.GET_UNINSTALLED_PACKAGES);
440        } catch (NameNotFoundException ignored) {
441        }
442
443        final int prefer;
444        final boolean checkBoth;
445        if ((installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
446            prefer = RECOMMEND_INSTALL_INTERNAL;
447            checkBoth = false;
448        } else if ((installFlags & PackageManager.INSTALL_EXTERNAL) != 0) {
449            prefer = RECOMMEND_INSTALL_EXTERNAL;
450            checkBoth = false;
451        } else if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
452            prefer = RECOMMEND_INSTALL_INTERNAL;
453            checkBoth = false;
454        } else if (installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
455            prefer = RECOMMEND_INSTALL_EXTERNAL;
456            checkBoth = true;
457        } else if (installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
458            // When app is already installed, prefer same medium
459            if (existingInfo != null) {
460                // TODO: distinguish if this is external ASEC
461                if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
462                    prefer = RECOMMEND_INSTALL_EXTERNAL;
463                } else {
464                    prefer = RECOMMEND_INSTALL_INTERNAL;
465                }
466            } else {
467                prefer = RECOMMEND_INSTALL_INTERNAL;
468            }
469            checkBoth = true;
470        } else {
471            prefer = RECOMMEND_INSTALL_INTERNAL;
472            checkBoth = false;
473        }
474
475        boolean fitsOnInternal = false;
476        if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) {
477            fitsOnInternal = fitsOnInternal(context, sizeBytes);
478        }
479
480        boolean fitsOnExternal = false;
481        if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) {
482            fitsOnExternal = fitsOnExternal(context, sizeBytes);
483        }
484
485        if (prefer == RECOMMEND_INSTALL_INTERNAL) {
486            if (fitsOnInternal) {
487                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
488            }
489        } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) {
490            if (fitsOnExternal) {
491                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
492            }
493        }
494
495        if (checkBoth) {
496            if (fitsOnInternal) {
497                return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
498            } else if (fitsOnExternal) {
499                return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
500            }
501        }
502
503        return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
504    }
505
506    public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked,
507            String abiOverride) throws IOException {
508        NativeLibraryHelper.Handle handle = null;
509        try {
510            handle = NativeLibraryHelper.Handle.create(pkg);
511            return calculateInstalledSize(pkg, handle, isForwardLocked, abiOverride);
512        } finally {
513            IoUtils.closeQuietly(handle);
514        }
515    }
516
517    public static long calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle,
518            boolean isForwardLocked, String abiOverride) throws IOException {
519        long sizeBytes = 0;
520
521        // Include raw APKs, and possibly unpacked resources
522        for (String codePath : pkg.getAllCodePaths()) {
523            final File codeFile = new File(codePath);
524            sizeBytes += codeFile.length();
525
526            if (isForwardLocked) {
527                sizeBytes += PackageHelper.extractPublicFiles(codeFile, null);
528            }
529        }
530
531        // Include all relevant native code
532        sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride);
533
534        return sizeBytes;
535    }
536
537    public static String replaceEnd(String str, String before, String after) {
538        if (!str.endsWith(before)) {
539            throw new IllegalArgumentException(
540                    "Expected " + str + " to end with " + before);
541        }
542        return str.substring(0, str.length() - before.length()) + after;
543    }
544}
545