1/*
2 * Copyright (C) 2017 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 android.app;
18
19import android.os.FileUtils;
20import android.os.RemoteException;
21import android.os.SystemProperties;
22import android.system.ErrnoException;
23import android.util.Slog;
24
25import com.android.internal.annotations.GuardedBy;
26
27import dalvik.system.BaseDexClassLoader;
28import dalvik.system.VMRuntime;
29
30import libcore.io.Libcore;
31
32import java.io.File;
33import java.io.IOException;
34import java.util.ArrayList;
35import java.util.HashSet;
36import java.util.List;
37import java.util.Set;
38
39/**
40 * A dex load reporter which will notify package manager of any dex file loaded
41 * with {@code BaseDexClassLoader}.
42 * The goals are:
43 *     1) discover secondary dex files so that they can be optimized during the
44 *        idle maintenance job.
45 *     2) determine whether or not a dex file is used by an app which does not
46 *        own it (in order to select the optimal compilation method).
47 * @hide
48 */
49/*package*/ class DexLoadReporter implements BaseDexClassLoader.Reporter {
50    private static final String TAG = "DexLoadReporter";
51
52    private static final DexLoadReporter INSTANCE = new DexLoadReporter();
53
54    private static final boolean DEBUG = false;
55
56    // We must guard the access to the list of data directories because
57    // we might have concurrent accesses. Apps might load dex files while
58    // new data dirs are registered (due to creation of LoadedApks via
59    // create createApplicationContext).
60    @GuardedBy("mDataDirs")
61    private final Set<String> mDataDirs;
62
63    private DexLoadReporter() {
64        mDataDirs = new HashSet<>();
65    }
66
67    /*package*/ static DexLoadReporter getInstance() {
68        return INSTANCE;
69    }
70
71    /**
72     * Register an application data directory with the reporter.
73     * The data directories are used to determine if a dex file is secondary dex or not.
74     * Note that this method may be called multiple times for the same app, registering
75     * different data directories. This may happen when apps share the same user id
76     * ({@code android:sharedUserId}). For example, if app1 and app2 share the same user
77     * id, and app1 loads app2 apk, then both data directories will be registered.
78     */
79    /*package*/ void registerAppDataDir(String packageName, String dataDir) {
80        if (DEBUG) {
81            Slog.i(TAG, "Package " + packageName + " registering data dir: " + dataDir);
82        }
83        // TODO(calin): A few code paths imply that the data dir
84        // might be null. Investigate when that can happen.
85        if (dataDir != null) {
86            synchronized (mDataDirs) {
87                mDataDirs.add(dataDir);
88            }
89        }
90    }
91
92    @Override
93    public void report(List<BaseDexClassLoader> classLoadersChain, List<String> classPaths) {
94        if (classLoadersChain.size() != classPaths.size()) {
95            Slog.wtf(TAG, "Bad call to DexLoadReporter: argument size mismatch");
96            return;
97        }
98        if (classPaths.isEmpty()) {
99            Slog.wtf(TAG, "Bad call to DexLoadReporter: empty dex paths");
100            return;
101        }
102
103        // The first element of classPaths is the list of dex files that should be registered.
104        // The classpath is represented as a list of dex files separated by File.pathSeparator.
105        String[] dexPathsForRegistration = classPaths.get(0).split(File.pathSeparator);
106        if (dexPathsForRegistration.length == 0) {
107            // No dex files to register.
108            return;
109        }
110
111        // Notify the package manager about the dex loads unconditionally.
112        // The load might be for either a primary or secondary dex file.
113        notifyPackageManager(classLoadersChain, classPaths);
114        // Check for secondary dex files and register them for profiling if possible.
115        // Note that we only register the dex paths belonging to the first class loader.
116        registerSecondaryDexForProfiling(dexPathsForRegistration);
117    }
118
119    private void notifyPackageManager(List<BaseDexClassLoader> classLoadersChain,
120            List<String> classPaths) {
121        // Get the class loader names for the binder call.
122        List<String> classLoadersNames = new ArrayList<>(classPaths.size());
123        for (BaseDexClassLoader bdc : classLoadersChain) {
124            classLoadersNames.add(bdc.getClass().getName());
125        }
126        String packageName = ActivityThread.currentPackageName();
127        try {
128            ActivityThread.getPackageManager().notifyDexLoad(
129                    packageName, classLoadersNames, classPaths,
130                    VMRuntime.getRuntime().vmInstructionSet());
131        } catch (RemoteException re) {
132            Slog.e(TAG, "Failed to notify PM about dex load for package " + packageName, re);
133        }
134    }
135
136    private void registerSecondaryDexForProfiling(String[] dexPaths) {
137        if (!SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)) {
138            return;
139        }
140        // Make a copy of the current data directories so that we don't keep the lock
141        // while registering for profiling. The registration will perform I/O to
142        // check for or create the profile.
143        String[] dataDirs;
144        synchronized (mDataDirs) {
145            dataDirs = mDataDirs.toArray(new String[0]);
146        }
147        for (String dexPath : dexPaths) {
148            registerSecondaryDexForProfiling(dexPath, dataDirs);
149        }
150    }
151
152    private void registerSecondaryDexForProfiling(String dexPath, String[] dataDirs) {
153        if (!isSecondaryDexFile(dexPath, dataDirs)) {
154            // The dex path is not a secondary dex file. Nothing to do.
155            return;
156        }
157
158        File realDexPath;
159        try {
160            // Secondary dex profiles are stored in the oat directory, next to the real dex file
161            // and have the same name with 'cur.prof' appended. We use the realpath because that
162            // is what installd is using when processing the dex file.
163            // NOTE: Keep in sync with installd.
164            realDexPath = new File(Libcore.os.realpath(dexPath));
165        } catch (ErrnoException ex) {
166            Slog.e(TAG, "Failed to get the real path of secondary dex " + dexPath
167                    + ":" + ex.getMessage());
168            // Do not continue with registration if we could not retrieve the real path.
169            return;
170        }
171
172        // NOTE: Keep this in sync with installd expectations.
173        File secondaryProfileDir = new File(realDexPath.getParent(), "oat");
174        File secondaryProfile = new File(secondaryProfileDir, realDexPath.getName() + ".cur.prof");
175
176        // Create the profile if not already there.
177        // Returns true if the file was created, false if the file already exists.
178        // or throws exceptions in case of errors.
179        if (!secondaryProfileDir.exists()) {
180            if (!secondaryProfileDir.mkdir()) {
181                Slog.e(TAG, "Could not create the profile directory: " + secondaryProfile);
182                // Do not continue with registration if we could not create the oat dir.
183                return;
184            }
185        }
186
187        try {
188            boolean created = secondaryProfile.createNewFile();
189            if (DEBUG && created) {
190                Slog.i(TAG, "Created profile for secondary dex: " + secondaryProfile);
191            }
192        } catch (IOException ex) {
193            Slog.e(TAG, "Failed to create profile for secondary dex " + dexPath
194                    + ":" + ex.getMessage());
195            // Do not continue with registration if we could not create the profile files.
196            return;
197        }
198
199        // If we got here, the dex paths is a secondary dex and we were able to create the profile.
200        // Register the path to the runtime.
201        VMRuntime.registerAppInfo(secondaryProfile.getPath(), new String[] { dexPath });
202    }
203
204    // A dex file is a secondary dex file if it is in any of the registered app
205    // data directories.
206    private boolean isSecondaryDexFile(String dexPath, String[] dataDirs) {
207        for (String dataDir : dataDirs) {
208            if (FileUtils.contains(dataDir, dexPath)) {
209                return true;
210            }
211        }
212        return false;
213    }
214}
215