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 
17 package com.android.internal.content;
18 
19 import static android.net.TrafficStats.MB_IN_BYTES;
20 
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageInfo;
24 import android.content.pm.PackageManager;
25 import android.content.pm.PackageManager.NameNotFoundException;
26 import android.content.pm.PackageParser.PackageLite;
27 import android.os.Environment;
28 import android.os.FileUtils;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.os.ServiceManager;
32 import android.os.storage.IMountService;
33 import android.os.storage.StorageManager;
34 import android.os.storage.StorageResultCode;
35 import android.os.storage.StorageVolume;
36 import android.os.storage.VolumeInfo;
37 import android.provider.Settings;
38 import android.util.ArraySet;
39 import android.util.Log;
40 
41 import libcore.io.IoUtils;
42 
43 import java.io.File;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.util.Collections;
48 import java.util.Objects;
49 import java.util.zip.ZipEntry;
50 import java.util.zip.ZipFile;
51 import 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  */
58 public 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 
getMountService()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 
createSdDir(long sizeBytes, String cid, String sdEncKey, int uid, boolean isExternal)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 
resizeSdDir(long sizeBytes, String cid, String sdEncKey)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 
mountSdDir(String cid, String key, int ownerUid)129     public static String mountSdDir(String cid, String key, int ownerUid) {
130         return mountSdDir(cid, key, ownerUid, true);
131     }
132 
mountSdDir(String cid, String key, int ownerUid, boolean readOnly)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 
unMountSdDir(String cid)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 
renameSdDir(String oldId, String newId)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 
getSdDir(String cid)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 
getSdFilesystem(String cid)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 
finalizeSdDir(String cid)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 
destroySdDir(String cid)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 
getSecureContainerList()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 
isContainerMounted(String cid)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      */
extractPublicFiles(File apkFile, File publicZipFile)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 
copyZipEntry(ZipEntry zipEntry, ZipFile inZipFile, ZipOutputStream outZipStream)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 
fixSdPermissions(String cid, int gid, String filename)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      */
resolveInstallVolume(Context context, String packageName, int installLocation, long sizeBytes)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 
fitsOnInternal(Context context, long sizeBytes)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 
fitsOnExternal(Context context, long sizeBytes)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      */
resolveInstallLocation(Context context, String packageName, int installLocation, long sizeBytes, int installFlags)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 
calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, String abiOverride)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 
calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, boolean isForwardLocked, String abiOverride)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 
replaceEnd(String str, String before, String after)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