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