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