1/*
2 * Copyright (C) 2010 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.defcontainer;
18
19import static com.android.internal.content.NativeLibraryHelper.LIB_DIR_NAME;
20
21import android.app.IntentService;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.IPackageManager;
25import android.content.pm.PackageCleanItem;
26import android.content.pm.PackageInfoLite;
27import android.content.pm.PackageManager;
28import android.content.pm.PackageParser;
29import android.content.pm.PackageParser.PackageLite;
30import android.content.pm.PackageParser.PackageParserException;
31import android.content.res.ObbInfo;
32import android.content.res.ObbScanner;
33import android.os.Environment;
34import android.os.Environment.UserEnvironment;
35import android.os.FileUtils;
36import android.os.IBinder;
37import android.os.ParcelFileDescriptor;
38import android.os.Process;
39import android.os.RemoteException;
40import android.os.ServiceManager;
41import android.system.ErrnoException;
42import android.system.Os;
43import android.system.StructStatVfs;
44import android.util.Slog;
45
46import com.android.internal.app.IMediaContainerService;
47import com.android.internal.content.NativeLibraryHelper;
48import com.android.internal.content.PackageHelper;
49import com.android.internal.os.IParcelFileDescriptorFactory;
50import com.android.internal.util.ArrayUtils;
51
52import libcore.io.IoUtils;
53import libcore.io.Streams;
54
55import java.io.File;
56import java.io.FileInputStream;
57import java.io.IOException;
58import java.io.InputStream;
59import java.io.OutputStream;
60
61/**
62 * Service that offers to inspect and copy files that may reside on removable
63 * storage. This is designed to prevent the system process from holding onto
64 * open files that cause the kernel to kill it when the underlying device is
65 * removed.
66 */
67public class DefaultContainerService extends IntentService {
68    private static final String TAG = "DefContainer";
69
70    // TODO: migrate native code unpacking to always be a derivative work
71
72    private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
73        /**
74         * Creates a new container and copies package there.
75         *
76         * @param packagePath absolute path to the package to be copied. Can be
77         *            a single monolithic APK file or a cluster directory
78         *            containing one or more APKs.
79         * @param containerId the id of the secure container that should be used
80         *            for creating a secure container into which the resource
81         *            will be copied.
82         * @param key Refers to key used for encrypting the secure container
83         * @return Returns the new cache path where the resource has been copied
84         *         into
85         */
86        @Override
87        public String copyPackageToContainer(String packagePath, String containerId, String key,
88                boolean isExternal, boolean isForwardLocked, String abiOverride) {
89            if (packagePath == null || containerId == null) {
90                return null;
91            }
92
93            if (isExternal) {
94                // Make sure the sdcard is mounted.
95                String status = Environment.getExternalStorageState();
96                if (!status.equals(Environment.MEDIA_MOUNTED)) {
97                    Slog.w(TAG, "Make sure sdcard is mounted.");
98                    return null;
99                }
100            }
101
102            PackageLite pkg = null;
103            NativeLibraryHelper.Handle handle = null;
104            try {
105                final File packageFile = new File(packagePath);
106                pkg = PackageParser.parsePackageLite(packageFile, 0);
107                handle = NativeLibraryHelper.Handle.create(pkg);
108                return copyPackageToContainerInner(pkg, handle, containerId, key, isExternal,
109                        isForwardLocked, abiOverride);
110            } catch (PackageParserException | IOException e) {
111                Slog.w(TAG, "Failed to copy package at " + packagePath, e);
112                return null;
113            } finally {
114                IoUtils.closeQuietly(handle);
115            }
116        }
117
118        /**
119         * Copy package to the target location.
120         *
121         * @param packagePath absolute path to the package to be copied. Can be
122         *            a single monolithic APK file or a cluster directory
123         *            containing one or more APKs.
124         * @return returns status code according to those in
125         *         {@link PackageManager}
126         */
127        @Override
128        public int copyPackage(String packagePath, IParcelFileDescriptorFactory target) {
129            if (packagePath == null || target == null) {
130                return PackageManager.INSTALL_FAILED_INVALID_URI;
131            }
132
133            PackageLite pkg = null;
134            try {
135                final File packageFile = new File(packagePath);
136                pkg = PackageParser.parsePackageLite(packageFile, 0);
137                return copyPackageInner(pkg, target);
138            } catch (PackageParserException | IOException | RemoteException e) {
139                Slog.w(TAG, "Failed to copy package at " + packagePath + ": " + e);
140                return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
141            }
142        }
143
144        /**
145         * Parse given package and return minimal details.
146         *
147         * @param packagePath absolute path to the package to be copied. Can be
148         *            a single monolithic APK file or a cluster directory
149         *            containing one or more APKs.
150         */
151        @Override
152        public PackageInfoLite getMinimalPackageInfo(String packagePath, int flags,
153                String abiOverride) {
154            final Context context = DefaultContainerService.this;
155            final boolean isForwardLocked = (flags & PackageManager.INSTALL_FORWARD_LOCK) != 0;
156
157            PackageInfoLite ret = new PackageInfoLite();
158            if (packagePath == null) {
159                Slog.i(TAG, "Invalid package file " + packagePath);
160                ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
161                return ret;
162            }
163
164            final File packageFile = new File(packagePath);
165            final PackageParser.PackageLite pkg;
166            final long sizeBytes;
167            try {
168                pkg = PackageParser.parsePackageLite(packageFile, 0);
169                sizeBytes = PackageHelper.calculateInstalledSize(pkg, isForwardLocked, abiOverride);
170            } catch (PackageParserException | IOException e) {
171                Slog.w(TAG, "Failed to parse package at " + packagePath + ": " + e);
172
173                if (!packageFile.exists()) {
174                    ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;
175                } else {
176                    ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;
177                }
178
179                return ret;
180            }
181
182            ret.packageName = pkg.packageName;
183            ret.splitNames = pkg.splitNames;
184            ret.versionCode = pkg.versionCode;
185            ret.baseRevisionCode = pkg.baseRevisionCode;
186            ret.splitRevisionCodes = pkg.splitRevisionCodes;
187            ret.installLocation = pkg.installLocation;
188            ret.verifiers = pkg.verifiers;
189            ret.recommendedInstallLocation = PackageHelper.resolveInstallLocation(context,
190                    pkg.packageName, pkg.installLocation, sizeBytes, flags);
191            ret.multiArch = pkg.multiArch;
192
193            return ret;
194        }
195
196        @Override
197        public ObbInfo getObbInfo(String filename) {
198            try {
199                return ObbScanner.getObbInfo(filename);
200            } catch (IOException e) {
201                Slog.d(TAG, "Couldn't get OBB info for " + filename);
202                return null;
203            }
204        }
205
206        @Override
207        public long calculateDirectorySize(String path) throws RemoteException {
208            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
209
210            final File dir = Environment.maybeTranslateEmulatedPathToInternal(new File(path));
211            if (dir.exists() && dir.isDirectory()) {
212                final String targetPath = dir.getAbsolutePath();
213                return MeasurementUtils.measureDirectory(targetPath);
214            } else {
215                return 0L;
216            }
217        }
218
219        @Override
220        public long[] getFileSystemStats(String path) {
221            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
222
223            try {
224                final StructStatVfs stat = Os.statvfs(path);
225                final long totalSize = stat.f_blocks * stat.f_bsize;
226                final long availSize = stat.f_bavail * stat.f_bsize;
227                return new long[] { totalSize, availSize };
228            } catch (ErrnoException e) {
229                throw new IllegalStateException(e);
230            }
231        }
232
233        @Override
234        public void clearDirectory(String path) throws RemoteException {
235            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
236
237            final File directory = new File(path);
238            if (directory.exists() && directory.isDirectory()) {
239                eraseFiles(directory);
240            }
241        }
242
243        /**
244         * Calculate estimated footprint of given package post-installation.
245         *
246         * @param packagePath absolute path to the package to be copied. Can be
247         *            a single monolithic APK file or a cluster directory
248         *            containing one or more APKs.
249         */
250        @Override
251        public long calculateInstalledSize(String packagePath, boolean isForwardLocked,
252                String abiOverride) throws RemoteException {
253            final File packageFile = new File(packagePath);
254            final PackageParser.PackageLite pkg;
255            try {
256                pkg = PackageParser.parsePackageLite(packageFile, 0);
257                return PackageHelper.calculateInstalledSize(pkg, isForwardLocked, abiOverride);
258            } catch (PackageParserException | IOException e) {
259                Slog.w(TAG, "Failed to calculate installed size: " + e);
260                return Long.MAX_VALUE;
261            }
262        }
263    };
264
265    public DefaultContainerService() {
266        super("DefaultContainerService");
267        setIntentRedelivery(true);
268    }
269
270    @Override
271    protected void onHandleIntent(Intent intent) {
272        if (PackageManager.ACTION_CLEAN_EXTERNAL_STORAGE.equals(intent.getAction())) {
273            final IPackageManager pm = IPackageManager.Stub.asInterface(
274                    ServiceManager.getService("package"));
275            PackageCleanItem item = null;
276            try {
277                while ((item = pm.nextPackageToClean(item)) != null) {
278                    final UserEnvironment userEnv = new UserEnvironment(item.userId);
279                    eraseFiles(userEnv.buildExternalStorageAppDataDirs(item.packageName));
280                    eraseFiles(userEnv.buildExternalStorageAppMediaDirs(item.packageName));
281                    if (item.andCode) {
282                        eraseFiles(userEnv.buildExternalStorageAppObbDirs(item.packageName));
283                    }
284                }
285            } catch (RemoteException e) {
286            }
287        }
288    }
289
290    void eraseFiles(File[] paths) {
291        for (File path : paths) {
292            eraseFiles(path);
293        }
294    }
295
296    void eraseFiles(File path) {
297        if (path.isDirectory()) {
298            String[] files = path.list();
299            if (files != null) {
300                for (String file : files) {
301                    eraseFiles(new File(path, file));
302                }
303            }
304        }
305        path.delete();
306    }
307
308    @Override
309    public IBinder onBind(Intent intent) {
310        return mBinder;
311    }
312
313    private String copyPackageToContainerInner(PackageLite pkg, NativeLibraryHelper.Handle handle,
314            String newCid, String key, boolean isExternal, boolean isForwardLocked,
315            String abiOverride) throws IOException {
316
317        // Calculate container size, rounding up to nearest MB and adding an
318        // extra MB for filesystem overhead
319        final long sizeBytes = PackageHelper.calculateInstalledSize(pkg, handle,
320                isForwardLocked, abiOverride);
321
322        // Create new container
323        final String newMountPath = PackageHelper.createSdDir(sizeBytes, newCid, key,
324                Process.myUid(), isExternal);
325        if (newMountPath == null) {
326            throw new IOException("Failed to create container " + newCid);
327        }
328        final File targetDir = new File(newMountPath);
329
330        try {
331            // Copy all APKs
332            copyFile(pkg.baseCodePath, targetDir, "base.apk", isForwardLocked);
333            if (!ArrayUtils.isEmpty(pkg.splitNames)) {
334                for (int i = 0; i < pkg.splitNames.length; i++) {
335                    copyFile(pkg.splitCodePaths[i], targetDir,
336                            "split_" + pkg.splitNames[i] + ".apk", isForwardLocked);
337                }
338            }
339
340            // Extract native code
341            final File libraryRoot = new File(targetDir, LIB_DIR_NAME);
342            final int res = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,
343                    abiOverride);
344            if (res != PackageManager.INSTALL_SUCCEEDED) {
345                throw new IOException("Failed to extract native code, res=" + res);
346            }
347
348            if (!PackageHelper.finalizeSdDir(newCid)) {
349                throw new IOException("Failed to finalize " + newCid);
350            }
351
352            if (PackageHelper.isContainerMounted(newCid)) {
353                PackageHelper.unMountSdDir(newCid);
354            }
355
356        } catch (ErrnoException e) {
357            PackageHelper.destroySdDir(newCid);
358            throw e.rethrowAsIOException();
359        } catch (IOException e) {
360            PackageHelper.destroySdDir(newCid);
361            throw e;
362        }
363
364        return newMountPath;
365    }
366
367    private int copyPackageInner(PackageLite pkg, IParcelFileDescriptorFactory target)
368            throws IOException, RemoteException {
369        copyFile(pkg.baseCodePath, target, "base.apk");
370        if (!ArrayUtils.isEmpty(pkg.splitNames)) {
371            for (int i = 0; i < pkg.splitNames.length; i++) {
372                copyFile(pkg.splitCodePaths[i], target, "split_" + pkg.splitNames[i] + ".apk");
373            }
374        }
375
376        return PackageManager.INSTALL_SUCCEEDED;
377    }
378
379    private void copyFile(String sourcePath, IParcelFileDescriptorFactory target, String targetName)
380            throws IOException, RemoteException {
381        Slog.d(TAG, "Copying " + sourcePath + " to " + targetName);
382        InputStream in = null;
383        OutputStream out = null;
384        try {
385            in = new FileInputStream(sourcePath);
386            out = new ParcelFileDescriptor.AutoCloseOutputStream(
387                    target.open(targetName, ParcelFileDescriptor.MODE_READ_WRITE));
388            Streams.copy(in, out);
389        } finally {
390            IoUtils.closeQuietly(out);
391            IoUtils.closeQuietly(in);
392        }
393    }
394
395    private void copyFile(String sourcePath, File targetDir, String targetName,
396            boolean isForwardLocked) throws IOException, ErrnoException {
397        final File sourceFile = new File(sourcePath);
398        final File targetFile = new File(targetDir, targetName);
399
400        Slog.d(TAG, "Copying " + sourceFile + " to " + targetFile);
401        if (!FileUtils.copyFile(sourceFile, targetFile)) {
402            throw new IOException("Failed to copy " + sourceFile + " to " + targetFile);
403        }
404
405        if (isForwardLocked) {
406            final String publicTargetName = PackageHelper.replaceEnd(targetName,
407                    ".apk", ".zip");
408            final File publicTargetFile = new File(targetDir, publicTargetName);
409
410            PackageHelper.extractPublicFiles(sourceFile, publicTargetFile);
411
412            Os.chmod(targetFile.getAbsolutePath(), 0640);
413            Os.chmod(publicTargetFile.getAbsolutePath(), 0644);
414        } else {
415            Os.chmod(targetFile.getAbsolutePath(), 0644);
416        }
417    }
418}
419