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.content.pm.dex.DexMetadataHelper;
29import android.os.Environment;
30import android.os.IBinder;
31import android.os.RemoteException;
32import android.os.ServiceManager;
33import android.os.storage.IStorageManager;
34import android.os.storage.StorageManager;
35import android.os.storage.StorageVolume;
36import android.os.storage.VolumeInfo;
37import android.provider.Settings;
38import android.util.ArraySet;
39import android.util.Log;
40
41import com.android.internal.annotations.VisibleForTesting;
42
43import libcore.io.IoUtils;
44
45import java.io.File;
46import java.io.FileDescriptor;
47import java.io.IOException;
48import java.util.Objects;
49import 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 */
56public 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
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 {
92        abstract public StorageManager getStorageManager(Context context);
93        abstract public boolean getForceAllowOnExternalSetting(Context context);
94        abstract public boolean getAllow3rdPartyOnInternalConfig(Context context);
95        abstract public ApplicationInfo getExistingAppInfo(Context context, String packageName);
96        abstract public File getDataDirectory();
97    }
98
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
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     */
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
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
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
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
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     */
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
375    public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked,
376            String abiOverride) throws IOException {
377        return calculateInstalledSize(pkg, abiOverride);
378    }
379
380    public static long calculateInstalledSize(PackageLite pkg, String abiOverride)
381            throws IOException {
382        return calculateInstalledSize(pkg, abiOverride, null);
383    }
384
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
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
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
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
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