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     /** {@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 
169     @VisibleForTesting
resolveInstallVolume(Context context, SessionParams params, TestableInterface testInterface)170     public static String resolveInstallVolume(Context context, SessionParams params,
171             TestableInterface testInterface) throws IOException {
172         final StorageManager storageManager = testInterface.getStorageManager(context);
173         final boolean forceAllowOnExternal = testInterface.getForceAllowOnExternalSetting(context);
174         final boolean allow3rdPartyOnInternal =
175                 testInterface.getAllow3rdPartyOnInternalConfig(context);
176         // TODO: handle existing apps installed in ASEC; currently assumes
177         // they'll end up back on internal storage
178         ApplicationInfo existingInfo = testInterface.getExistingAppInfo(context,
179                 params.appPackageName);
180 
181         // Figure out best candidate volume, and also if we fit on internal
182         final ArraySet<String> allCandidates = new ArraySet<>();
183         boolean fitsOnInternal = false;
184         VolumeInfo bestCandidate = null;
185         long bestCandidateAvailBytes = Long.MIN_VALUE;
186         for (VolumeInfo vol : storageManager.getVolumes()) {
187             if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
188                 final boolean isInternalStorage = ID_PRIVATE_INTERNAL.equals(vol.id);
189                 final UUID target = storageManager.getUuidForPath(new File(vol.path));
190                 final long availBytes = storageManager.getAllocatableBytes(target,
191                         translateAllocateFlags(params.installFlags));
192                 if (isInternalStorage) {
193                     fitsOnInternal = (params.sizeBytes <= availBytes);
194                 }
195                 if (!isInternalStorage || allow3rdPartyOnInternal) {
196                     if (availBytes >= params.sizeBytes) {
197                         allCandidates.add(vol.fsUuid);
198                     }
199                     if (availBytes >= bestCandidateAvailBytes) {
200                         bestCandidate = vol;
201                         bestCandidateAvailBytes = availBytes;
202                     }
203                 }
204             }
205         }
206 
207         // System apps always forced to internal storage
208         if (existingInfo != null && existingInfo.isSystemApp()) {
209             if (fitsOnInternal) {
210                 return StorageManager.UUID_PRIVATE_INTERNAL;
211             } else {
212                 throw new IOException("Not enough space on existing volume "
213                         + existingInfo.volumeUuid + " for system app " + params.appPackageName
214                         + " upgrade");
215             }
216         }
217 
218         // If app expresses strong desire for internal storage, honor it
219         if (!forceAllowOnExternal
220                 && params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
221             if (existingInfo != null && !Objects.equals(existingInfo.volumeUuid,
222                     StorageManager.UUID_PRIVATE_INTERNAL)) {
223                 throw new IOException("Cannot automatically move " + params.appPackageName
224                         + " from " + existingInfo.volumeUuid + " to internal storage");
225             }
226 
227             if (!allow3rdPartyOnInternal) {
228                 throw new IOException("Not allowed to install non-system apps on internal storage");
229             }
230 
231             if (fitsOnInternal) {
232                 return StorageManager.UUID_PRIVATE_INTERNAL;
233             } else {
234                 throw new IOException("Requested internal only, but not enough space");
235             }
236         }
237 
238         // If app already exists somewhere, we must stay on that volume
239         if (existingInfo != null) {
240             if (Objects.equals(existingInfo.volumeUuid, StorageManager.UUID_PRIVATE_INTERNAL)
241                     && fitsOnInternal) {
242                 return StorageManager.UUID_PRIVATE_INTERNAL;
243             } else if (allCandidates.contains(existingInfo.volumeUuid)) {
244                 return existingInfo.volumeUuid;
245             } else {
246                 throw new IOException("Not enough space on existing volume "
247                         + existingInfo.volumeUuid + " for " + params.appPackageName + " upgrade");
248             }
249         }
250 
251         // We're left with new installations with either preferring external or auto, so just pick
252         // volume with most space
253         if (bestCandidate != null) {
254             return bestCandidate.fsUuid;
255         } else {
256             throw new IOException("No special requests, but no room on allowed volumes. "
257                 + " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal);
258         }
259     }
260 
fitsOnInternal(Context context, SessionParams params)261     public static boolean fitsOnInternal(Context context, SessionParams params) throws IOException {
262         final StorageManager storage = context.getSystemService(StorageManager.class);
263         final UUID target = storage.getUuidForPath(Environment.getDataDirectory());
264         return (params.sizeBytes <= storage.getAllocatableBytes(target,
265                 translateAllocateFlags(params.installFlags)));
266     }
267 
fitsOnExternal(Context context, SessionParams params)268     public static boolean fitsOnExternal(Context context, SessionParams params) {
269         final StorageManager storage = context.getSystemService(StorageManager.class);
270         final StorageVolume primary = storage.getPrimaryVolume();
271         return (params.sizeBytes > 0) && !primary.isEmulated()
272                 && Environment.MEDIA_MOUNTED.equals(primary.getState())
273                 && params.sizeBytes <= storage.getStorageBytesUntilLow(primary.getPathFile());
274     }
275 
276     @Deprecated
resolveInstallLocation(Context context, String packageName, int installLocation, long sizeBytes, int installFlags)277     public static int resolveInstallLocation(Context context, String packageName,
278             int installLocation, long sizeBytes, int installFlags) {
279         final SessionParams params = new SessionParams(SessionParams.MODE_INVALID);
280         params.appPackageName = packageName;
281         params.installLocation = installLocation;
282         params.sizeBytes = sizeBytes;
283         params.installFlags = installFlags;
284         try {
285             return resolveInstallLocation(context, params);
286         } catch (IOException e) {
287             throw new IllegalStateException(e);
288         }
289     }
290 
291     /**
292      * Given a requested {@link PackageInfo#installLocation} and calculated
293      * install size, pick the actual location to install the app.
294      */
resolveInstallLocation(Context context, SessionParams params)295     public static int resolveInstallLocation(Context context, SessionParams params)
296             throws IOException {
297         ApplicationInfo existingInfo = null;
298         try {
299             existingInfo = context.getPackageManager().getApplicationInfo(params.appPackageName,
300                     PackageManager.MATCH_ANY_USER);
301         } catch (NameNotFoundException ignored) {
302         }
303 
304         final int prefer;
305         final boolean checkBoth;
306         boolean ephemeral = false;
307         if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
308             prefer = RECOMMEND_INSTALL_INTERNAL;
309             ephemeral = true;
310             checkBoth = false;
311         } else if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
312             prefer = RECOMMEND_INSTALL_INTERNAL;
313             checkBoth = false;
314         } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
315             prefer = RECOMMEND_INSTALL_INTERNAL;
316             checkBoth = false;
317         } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
318             prefer = RECOMMEND_INSTALL_EXTERNAL;
319             checkBoth = true;
320         } else if (params.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {
321             // When app is already installed, prefer same medium
322             if (existingInfo != null) {
323                 // TODO: distinguish if this is external ASEC
324                 if ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {
325                     prefer = RECOMMEND_INSTALL_EXTERNAL;
326                 } else {
327                     prefer = RECOMMEND_INSTALL_INTERNAL;
328                 }
329             } else {
330                 prefer = RECOMMEND_INSTALL_INTERNAL;
331             }
332             checkBoth = true;
333         } else {
334             prefer = RECOMMEND_INSTALL_INTERNAL;
335             checkBoth = false;
336         }
337 
338         boolean fitsOnInternal = false;
339         if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) {
340             fitsOnInternal = fitsOnInternal(context, params);
341         }
342 
343         boolean fitsOnExternal = false;
344         if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) {
345             fitsOnExternal = fitsOnExternal(context, params);
346         }
347 
348         if (prefer == RECOMMEND_INSTALL_INTERNAL) {
349             // The ephemeral case will either fit and return EPHEMERAL, or will not fit
350             // and will fall through to return INSUFFICIENT_STORAGE
351             if (fitsOnInternal) {
352                 return (ephemeral)
353                         ? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL
354                         : PackageHelper.RECOMMEND_INSTALL_INTERNAL;
355             }
356         } else if (prefer == RECOMMEND_INSTALL_EXTERNAL) {
357             if (fitsOnExternal) {
358                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
359             }
360         }
361 
362         if (checkBoth) {
363             if (fitsOnInternal) {
364                 return PackageHelper.RECOMMEND_INSTALL_INTERNAL;
365             } else if (fitsOnExternal) {
366                 return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;
367             }
368         }
369 
370         return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
371     }
372 
373     @Deprecated
calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, String abiOverride)374     public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked,
375             String abiOverride) throws IOException {
376         return calculateInstalledSize(pkg, abiOverride);
377     }
378 
calculateInstalledSize(PackageLite pkg, String abiOverride)379     public static long calculateInstalledSize(PackageLite pkg, String abiOverride)
380             throws IOException {
381         return calculateInstalledSize(pkg, abiOverride, null);
382     }
383 
calculateInstalledSize(PackageLite pkg, String abiOverride, FileDescriptor fd)384     public static long calculateInstalledSize(PackageLite pkg, String abiOverride,
385             FileDescriptor fd) throws IOException {
386         NativeLibraryHelper.Handle handle = null;
387         try {
388             handle = fd != null ? NativeLibraryHelper.Handle.createFd(pkg, fd)
389                     : NativeLibraryHelper.Handle.create(pkg);
390             return calculateInstalledSize(pkg, handle, abiOverride);
391         } finally {
392             IoUtils.closeQuietly(handle);
393         }
394     }
395 
396     @Deprecated
calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, NativeLibraryHelper.Handle handle, String abiOverride)397     public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked,
398             NativeLibraryHelper.Handle handle, String abiOverride) throws IOException {
399         return calculateInstalledSize(pkg, handle, abiOverride);
400     }
401 
calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, String abiOverride)402     public static long calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle,
403             String abiOverride) throws IOException {
404         long sizeBytes = 0;
405 
406         // Include raw APKs, and possibly unpacked resources
407         for (String codePath : pkg.getAllCodePaths()) {
408             final File codeFile = new File(codePath);
409             sizeBytes += codeFile.length();
410         }
411 
412         // Include raw dex metadata files
413         sizeBytes += DexMetadataHelper.getPackageDexMetadataSize(pkg);
414 
415         // Include all relevant native code
416         sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride);
417 
418         return sizeBytes;
419     }
420 
replaceEnd(String str, String before, String after)421     public static String replaceEnd(String str, String before, String after) {
422         if (!str.endsWith(before)) {
423             throw new IllegalArgumentException(
424                     "Expected " + str + " to end with " + before);
425         }
426         return str.substring(0, str.length() - before.length()) + after;
427     }
428 
translateAllocateFlags(int installFlags)429     public static int translateAllocateFlags(int installFlags) {
430         if ((installFlags & PackageManager.INSTALL_ALLOCATE_AGGRESSIVE) != 0) {
431             return StorageManager.FLAG_ALLOCATE_AGGRESSIVE;
432         } else {
433             return 0;
434         }
435     }
436 }
437