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.os.storage.VolumeInfo.ID_PRIVATE_INTERNAL;
20 
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageInfo;
24 import android.content.pm.PackageInstaller.SessionParams;
25 import android.content.pm.PackageManager;
26 import android.content.pm.PackageManager.NameNotFoundException;
27 import android.content.pm.PackageParser.PackageLite;
28 import android.content.pm.dex.DexMetadataHelper;
29 import android.os.Environment;
30 import android.os.IBinder;
31 import android.os.RemoteException;
32 import android.os.ServiceManager;
33 import android.os.storage.IStorageManager;
34 import android.os.storage.StorageManager;
35 import android.os.storage.StorageVolume;
36 import android.os.storage.VolumeInfo;
37 import android.provider.Settings;
38 import android.util.ArrayMap;
39 import android.util.Log;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 
43 import libcore.io.IoUtils;
44 
45 import java.io.File;
46 import java.io.FileDescriptor;
47 import java.io.IOException;
48 import java.util.Objects;
49 import java.util.UUID;
50 
51 /**
52  * Constants used internally between the PackageManager
53  * and media container service transports.
54  * Some utility methods to invoke StorageManagerService 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_INSTALL_EPHEMERAL = 3;
60     public static final int RECOMMEND_FAILED_INSUFFICIENT_STORAGE = -1;
61     public static final int RECOMMEND_FAILED_INVALID_APK = -2;
62     public static final int RECOMMEND_FAILED_INVALID_LOCATION = -3;
63     public static final int RECOMMEND_FAILED_ALREADY_EXISTS = -4;
64     public static final int RECOMMEND_MEDIA_UNAVAILABLE = -5;
65     public static final int RECOMMEND_FAILED_INVALID_URI = -6;
66     public static final int RECOMMEND_FAILED_VERSION_DOWNGRADE = -7;
67     /** {@hide} */
68     public static final int RECOMMEND_FAILED_WRONG_INSTALLED_VERSION = -8;
69 
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     private static TestableInterface sDefaultTestableInterface = null;
77 
getStorageManager()78     public static IStorageManager getStorageManager() throws RemoteException {
79         IBinder service = ServiceManager.getService("mount");
80         if (service != null) {
81             return IStorageManager.Stub.asInterface(service);
82         } else {
83             Log.e(TAG, "Can't get storagemanager service");
84             throw new RemoteException("Could not contact storagemanager service");
85         }
86     }
87 
88     /**
89      * A group of external dependencies used in
90      * {@link #resolveInstallVolume(Context, String, int, long)}. It can be backed by real values
91      * from the system or mocked ones for testing purposes.
92      */
93     public static abstract class TestableInterface {
getStorageManager(Context context)94         abstract public StorageManager getStorageManager(Context context);
getForceAllowOnExternalSetting(Context context)95         abstract public boolean getForceAllowOnExternalSetting(Context context);
getAllow3rdPartyOnInternalConfig(Context context)96         abstract public boolean getAllow3rdPartyOnInternalConfig(Context context);
getExistingAppInfo(Context context, String packageName)97         abstract public ApplicationInfo getExistingAppInfo(Context context, String packageName);
getDataDirectory()98         abstract public File getDataDirectory();
99     }
100 
getDefaultTestableInterface()101     private synchronized static TestableInterface getDefaultTestableInterface() {
102         if (sDefaultTestableInterface == null) {
103             sDefaultTestableInterface = new TestableInterface() {
104                 @Override
105                 public StorageManager getStorageManager(Context context) {
106                     return context.getSystemService(StorageManager.class);
107                 }
108 
109                 @Override
110                 public boolean getForceAllowOnExternalSetting(Context context) {
111                     return Settings.Global.getInt(context.getContentResolver(),
112                             Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0;
113                 }
114 
115                 @Override
116                 public boolean getAllow3rdPartyOnInternalConfig(Context context) {
117                     return context.getResources().getBoolean(
118                             com.android.internal.R.bool.config_allow3rdPartyAppOnInternal);
119                 }
120 
121                 @Override
122                 public ApplicationInfo getExistingAppInfo(Context context, String packageName) {
123                     ApplicationInfo existingInfo = null;
124                     try {
125                         existingInfo = context.getPackageManager().getApplicationInfo(packageName,
126                                 PackageManager.MATCH_ANY_USER);
127                     } catch (NameNotFoundException ignored) {
128                     }
129                     return existingInfo;
130                 }
131 
132                 @Override
133                 public File getDataDirectory() {
134                     return Environment.getDataDirectory();
135                 }
136             };
137         }
138         return sDefaultTestableInterface;
139     }
140 
141     @VisibleForTesting
142     @Deprecated
resolveInstallVolume(Context context, String packageName, int installLocation, long sizeBytes, TestableInterface testInterface)143     public static String resolveInstallVolume(Context context, String packageName,
144             int installLocation, long sizeBytes, TestableInterface testInterface)
145             throws IOException {
146         final SessionParams params = new SessionParams(SessionParams.MODE_INVALID);
147         params.appPackageName = packageName;
148         params.installLocation = installLocation;
149         params.sizeBytes = sizeBytes;
150         return resolveInstallVolume(context, params, testInterface);
151     }
152 
153     /**
154      * Given a requested {@link PackageInfo#installLocation} and calculated
155      * install size, pick the actual volume to install the app. Only considers
156      * internal and private volumes, and prefers to keep an existing package on
157      * its current volume.
158      *
159      * @return the {@link VolumeInfo#fsUuid} to install onto, or {@code null}
160      *         for internal storage.
161      */
resolveInstallVolume(Context context, SessionParams params)162     public static String resolveInstallVolume(Context context, SessionParams params)
163             throws IOException {
164         TestableInterface testableInterface = getDefaultTestableInterface();
165         return resolveInstallVolume(context, params.appPackageName, params.installLocation,
166                 params.sizeBytes, testableInterface);
167     }
168 
checkFitOnVolume(StorageManager storageManager, String volumePath, SessionParams params)169     private static boolean checkFitOnVolume(StorageManager storageManager, String volumePath,
170             SessionParams params) throws IOException {
171         if (volumePath == null) {
172             return false;
173         }
174         final int installFlags = translateAllocateFlags(params.installFlags);
175         final UUID target = storageManager.getUuidForPath(new File(volumePath));
176         final long availBytes = storageManager.getAllocatableBytes(target,
177                 installFlags | StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY);
178         if (params.sizeBytes <= availBytes) {
179             return true;
180         }
181         final long cacheClearable = storageManager.getAllocatableBytes(target,
182                 installFlags | StorageManager.FLAG_ALLOCATE_CACHE_ONLY);
183         return params.sizeBytes <= availBytes + cacheClearable;
184     }
185 
186     @VisibleForTesting
resolveInstallVolume(Context context, SessionParams params, TestableInterface testInterface)187     public static String resolveInstallVolume(Context context, SessionParams params,
188             TestableInterface testInterface) throws IOException {
189         final StorageManager storageManager = testInterface.getStorageManager(context);
190         final boolean forceAllowOnExternal = testInterface.getForceAllowOnExternalSetting(context);
191         final boolean allow3rdPartyOnInternal =
192                 testInterface.getAllow3rdPartyOnInternalConfig(context);
193         // TODO: handle existing apps installed in ASEC; currently assumes
194         // they'll end up back on internal storage
195         ApplicationInfo existingInfo = testInterface.getExistingAppInfo(context,
196                 params.appPackageName);
197 
198         final ArrayMap<String, String> volumePaths = new ArrayMap<>();
199         String internalVolumePath = null;
200         for (VolumeInfo vol : storageManager.getVolumes()) {
201             if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
202                 final boolean isInternalStorage = ID_PRIVATE_INTERNAL.equals(vol.id);
203                 if (isInternalStorage) {
204                     internalVolumePath = vol.path;
205                 }
206                 if (!isInternalStorage || allow3rdPartyOnInternal) {
207                     volumePaths.put(vol.fsUuid, vol.path);
208                 }
209             }
210         }
211 
212         // System apps always forced to internal storage
213         if (existingInfo != null && existingInfo.isSystemApp()) {
214             if (checkFitOnVolume(storageManager, internalVolumePath, params)) {
215                 return StorageManager.UUID_PRIVATE_INTERNAL;
216             } else {
217                 throw new IOException("Not enough space on existing volume "
218                         + existingInfo.volumeUuid + " for system app " + params.appPackageName
219                         + " upgrade");
220             }
221         }
222 
223         // If app expresses strong desire for internal storage, honor it
224         if (!forceAllowOnExternal
225                 && params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
226             if (existingInfo != null && !Objects.equals(existingInfo.volumeUuid,
227                     StorageManager.UUID_PRIVATE_INTERNAL)) {
228                 throw new IOException("Cannot automatically move " + params.appPackageName
229                         + " from " + existingInfo.volumeUuid + " to internal storage");
230             }
231 
232             if (!allow3rdPartyOnInternal) {
233                 throw new IOException("Not allowed to install non-system apps on internal storage");
234             }
235 
236             if (checkFitOnVolume(storageManager, internalVolumePath, params)) {
237                 return StorageManager.UUID_PRIVATE_INTERNAL;
238             } else {
239                 throw new IOException("Requested internal only, but not enough space");
240             }
241         }
242 
243         // If app already exists somewhere, we must stay on that volume
244         if (existingInfo != null) {
245             String existingVolumePath = null;
246             if (Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)) {
247                 existingVolumePath = internalVolumePath;
248             } else if (volumePaths.containsKey(existingInfo.volumeUuid)) {
249                 existingVolumePath = volumePaths.get(existingInfo.volumeUuid);
250             }
251 
252             if (checkFitOnVolume(storageManager, existingVolumePath, params)) {
253                 return existingInfo.volumeUuid;
254             } else {
255                 throw new IOException("Not enough space on existing volume "
256                         + existingInfo.volumeUuid + " for " + params.appPackageName + " upgrade");
257             }
258         }
259 
260         // We're left with new installations with either preferring external or auto, so just pick
261         // volume with most space
262         if (volumePaths.size() == 1) {
263             if (checkFitOnVolume(storageManager, volumePaths.valueAt(0), params)) {
264                 return volumePaths.keyAt(0);
265             }
266         } else {
267             String bestCandidate = null;
268             long bestCandidateAvailBytes = Long.MIN_VALUE;
269             for (String vol : volumePaths.keySet()) {
270                 final String volumePath = volumePaths.get(vol);
271                 final UUID target = storageManager.getUuidForPath(new File(volumePath));
272 
273                 // We need to take into account freeable cached space, because we're choosing the
274                 // best candidate amongst a list, not just checking if we fit at all.
275                 final long availBytes = storageManager.getAllocatableBytes(target,
276                         translateAllocateFlags(params.installFlags));
277 
278                 if (availBytes >= bestCandidateAvailBytes) {
279                     bestCandidate = vol;
280                     bestCandidateAvailBytes = availBytes;
281                 }
282             }
283 
284             if (bestCandidateAvailBytes >= params.sizeBytes) {
285                 return bestCandidate;
286             }
287 
288         }
289 
290         throw new IOException("No special requests, but no room on allowed volumes. "
291                 + " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal);
292     }
293 
fitsOnInternal(Context context, SessionParams params)294     public static boolean fitsOnInternal(Context context, SessionParams params) throws IOException {
295         final StorageManager storage = context.getSystemService(StorageManager.class);
296         final UUID target = storage.getUuidForPath(Environment.getDataDirectory());
297         final int flags = translateAllocateFlags(params.installFlags);
298 
299         final long allocateableBytes = storage.getAllocatableBytes(target,
300                 flags | StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY);
301 
302         // If we fit on internal storage without including freeable cache space, don't bother
303         // checking to determine how much space is taken up by the cache.
304         if (params.sizeBytes <= allocateableBytes) {
305             return true;
306         }
307 
308         final long cacheClearable = storage.getAllocatableBytes(target,
309                 flags | StorageManager.FLAG_ALLOCATE_CACHE_ONLY);
310 
311         return params.sizeBytes <= allocateableBytes + cacheClearable;
312     }
313 
fitsOnExternal(Context context, SessionParams params)314     public static boolean fitsOnExternal(Context context, SessionParams params) {
315         final StorageManager storage = context.getSystemService(StorageManager.class);
316         final StorageVolume primary = storage.getPrimaryVolume();
317         return (params.sizeBytes > 0) && !primary.isEmulated()
318                 && Environment.MEDIA_MOUNTED.equals(primary.getState())
319                 && params.sizeBytes <= storage.getStorageBytesUntilLow(primary.getPathFile());
320     }
321 
322     @Deprecated
resolveInstallLocation(Context context, String packageName, int installLocation, long sizeBytes, int installFlags)323     public static int resolveInstallLocation(Context context, String packageName,
324             int installLocation, long sizeBytes, int installFlags) {
325         final SessionParams params = new SessionParams(SessionParams.MODE_INVALID);
326         params.appPackageName = packageName;
327         params.installLocation = installLocation;
328         params.sizeBytes = sizeBytes;
329         params.installFlags = installFlags;
330         try {
331             return resolveInstallLocation(context, params);
332         } catch (IOException e) {
333             throw new IllegalStateException(e);
334         }
335     }
336 
337     /**
338      * Given a requested {@link PackageInfo#installLocation} and calculated
339      * install size, pick the actual location to install the app.
340      */
resolveInstallLocation(Context context, SessionParams params)341     public static int resolveInstallLocation(Context context, SessionParams params)
342             throws IOException {
343         ApplicationInfo existingInfo = null;
344         try {
345             existingInfo = context.getPackageManager().getApplicationInfo(params.appPackageName,
346                     PackageManager.MATCH_ANY_USER);
347         } catch (NameNotFoundException ignored) {
348         }
349 
350         final int prefer;
351         final boolean checkBoth;
352         boolean ephemeral = false;
353         if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
354             prefer = RECOMMEND_INSTALL_INTERNAL;
355             ephemeral = true;
356             checkBoth = false;
357         } else if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
358             prefer = RECOMMEND_INSTALL_INTERNAL;
359             checkBoth = false;
360         } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
361             prefer = RECOMMEND_INSTALL_INTERNAL;
362             checkBoth = false;
363         } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
364             prefer = RECOMMEND_INSTALL_EXTERNAL;
365             checkBoth = true;
366         } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
367             // When app is already installed, prefer same medium
368             if (existingInfo != null) {
369                 // TODO: distinguish if this is external ASEC
370                 if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
371                     prefer = RECOMMEND_INSTALL_EXTERNAL;
372                 } else {
373                     prefer = RECOMMEND_INSTALL_INTERNAL;
374                 }
375             } else {
376                 prefer = RECOMMEND_INSTALL_INTERNAL;
377             }
378             checkBoth = true;
379         } else {
380             prefer = RECOMMEND_INSTALL_INTERNAL;
381             checkBoth = false;
382         }
383 
384         boolean fitsOnInternal = false;
385         if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) {
386             fitsOnInternal = fitsOnInternal(context, params);
387         }
388 
389         boolean fitsOnExternal = false;
390         if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) {
391             fitsOnExternal = fitsOnExternal(context, params);
392         }
393 
394         if (prefer == RECOMMEND_INSTALL_INTERNAL) {
395             // The ephemeral case will either fit and return EPHEMERAL, or will not fit
396             // and will fall through to return INSUFFICIENT_STORAGE
397             if (fitsOnInternal) {
398                 return (ephemeral)
399                         ? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL
400                         : PackageHelper.RECOMMEND_INSTALL_INTERNAL;
401             }
402         } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) {
403             if (fitsOnExternal) {
404                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
405             }
406         }
407 
408         if (checkBoth) {
409             if (fitsOnInternal) {
410                 return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
411             } else if (fitsOnExternal) {
412                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
413             }
414         }
415 
416         return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
417     }
418 
419     @Deprecated
calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, String abiOverride)420     public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked,
421             String abiOverride) throws IOException {
422         return calculateInstalledSize(pkg, abiOverride);
423     }
424 
calculateInstalledSize(PackageLite pkg, String abiOverride)425     public static long calculateInstalledSize(PackageLite pkg, String abiOverride)
426             throws IOException {
427         return calculateInstalledSize(pkg, abiOverride, null);
428     }
429 
calculateInstalledSize(PackageLite pkg, String abiOverride, FileDescriptor fd)430     public static long calculateInstalledSize(PackageLite pkg, String abiOverride,
431             FileDescriptor fd) throws IOException {
432         NativeLibraryHelper.Handle handle = null;
433         try {
434             handle = fd != null ? NativeLibraryHelper.Handle.createFd(pkg, fd)
435                     : NativeLibraryHelper.Handle.create(pkg);
436             return calculateInstalledSize(pkg, handle, abiOverride);
437         } finally {
438             IoUtils.closeQuietly(handle);
439         }
440     }
441 
442     @Deprecated
calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, NativeLibraryHelper.Handle handle, String abiOverride)443     public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked,
444             NativeLibraryHelper.Handle handle, String abiOverride) throws IOException {
445         return calculateInstalledSize(pkg, handle, abiOverride);
446     }
447 
calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, String abiOverride)448     public static long calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle,
449             String abiOverride) throws IOException {
450         long sizeBytes = 0;
451 
452         // Include raw APKs, and possibly unpacked resources
453         for (String codePath : pkg.getAllCodePaths()) {
454             final File codeFile = new File(codePath);
455             sizeBytes += codeFile.length();
456         }
457 
458         // Include raw dex metadata files
459         sizeBytes += DexMetadataHelper.getPackageDexMetadataSize(pkg);
460 
461         // Include all relevant native code
462         sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride);
463 
464         return sizeBytes;
465     }
466 
replaceEnd(String str, String before, String after)467     public static String replaceEnd(String str, String before, String after) {
468         if (!str.endsWith(before)) {
469             throw new IllegalArgumentException(
470                     "Expected " + str + " to end with " + before);
471         }
472         return str.substring(0, str.length() - before.length()) + after;
473     }
474 
translateAllocateFlags(int installFlags)475     public static int translateAllocateFlags(int installFlags) {
476         if ((installFlags & PackageManager.INSTALL_ALLOCATE_AGGRESSIVE) != 0) {
477             return StorageManager.FLAG_ALLOCATE_AGGRESSIVE;
478         } else {
479             return 0;
480         }
481     }
482 }
483