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