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