1/*
2 * Copyright (C) 2016 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.server.pm.dex;
18
19import android.util.AtomicFile;
20import android.util.Slog;
21import android.os.Build;
22
23import com.android.internal.annotations.GuardedBy;
24import com.android.internal.util.FastPrintWriter;
25import com.android.server.pm.AbstractStatsBase;
26import com.android.server.pm.PackageManagerServiceUtils;
27
28import java.io.BufferedReader;
29import java.io.File;
30import java.io.FileNotFoundException;
31import java.io.FileOutputStream;
32import java.io.InputStreamReader;
33import java.io.IOException;
34import java.io.OutputStreamWriter;
35import java.io.Reader;
36import java.io.StringWriter;
37import java.io.Writer;
38import java.util.Iterator;
39import java.util.HashMap;
40import java.util.HashSet;
41import java.util.Map;
42import java.util.Set;
43
44import dalvik.system.VMRuntime;
45import libcore.io.IoUtils;
46
47/**
48 * Stat file which store usage information about dex files.
49 */
50public class PackageDexUsage extends AbstractStatsBase<Void> {
51    private final static String TAG = "PackageDexUsage";
52
53    private final static int PACKAGE_DEX_USAGE_VERSION = 1;
54    private final static String PACKAGE_DEX_USAGE_VERSION_HEADER =
55            "PACKAGE_MANAGER__PACKAGE_DEX_USAGE__";
56
57    private final static String SPLIT_CHAR = ",";
58    private final static String DEX_LINE_CHAR = "#";
59
60    // Map which structures the information we have on a package.
61    // Maps package name to package data (which stores info about UsedByOtherApps and
62    // secondary dex files.).
63    // Access to this map needs synchronized.
64    @GuardedBy("mPackageUseInfoMap")
65    private Map<String, PackageUseInfo> mPackageUseInfoMap;
66
67    public PackageDexUsage() {
68        super("package-dex-usage.list", "PackageDexUsage_DiskWriter", /*lock*/ false);
69        mPackageUseInfoMap = new HashMap<>();
70    }
71
72    /**
73     * Record a dex file load.
74     *
75     * Note this is called when apps load dex files and as such it should return
76     * as fast as possible.
77     *
78     * @param loadingPackage the package performing the load
79     * @param dexPath the path of the dex files being loaded
80     * @param ownerUserId the user id which runs the code loading the dex files
81     * @param loaderIsa the ISA of the app loading the dex files
82     * @param isUsedByOtherApps whether or not this dex file was not loaded by its owning package
83     * @param primaryOrSplit whether or not the dex file is a primary/split dex. True indicates
84     *        the file is either primary or a split. False indicates the file is secondary dex.
85     * @return true if the dex load constitutes new information, or false if this information
86     *         has been seen before.
87     */
88    public boolean record(String owningPackageName, String dexPath, int ownerUserId,
89            String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit) {
90        if (!PackageManagerServiceUtils.checkISA(loaderIsa)) {
91            throw new IllegalArgumentException("loaderIsa " + loaderIsa + " is unsupported");
92        }
93        synchronized (mPackageUseInfoMap) {
94            PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(owningPackageName);
95            if (packageUseInfo == null) {
96                // This is the first time we see the package.
97                packageUseInfo = new PackageUseInfo();
98                if (primaryOrSplit) {
99                    // If we have a primary or a split apk, set isUsedByOtherApps.
100                    // We do not need to record the loaderIsa or the owner because we compile
101                    // primaries for all users and all ISAs.
102                    packageUseInfo.mIsUsedByOtherApps = isUsedByOtherApps;
103                } else {
104                    // For secondary dex files record the loaderISA and the owner. We'll need
105                    // to know under which user to compile and for what ISA.
106                    packageUseInfo.mDexUseInfoMap.put(
107                            dexPath, new DexUseInfo(isUsedByOtherApps, ownerUserId, loaderIsa));
108                }
109                mPackageUseInfoMap.put(owningPackageName, packageUseInfo);
110                return true;
111            } else {
112                // We already have data on this package. Amend it.
113                if (primaryOrSplit) {
114                    // We have a possible update on the primary apk usage. Merge
115                    // isUsedByOtherApps information and return if there was an update.
116                    return packageUseInfo.merge(isUsedByOtherApps);
117                } else {
118                    DexUseInfo newData = new DexUseInfo(
119                            isUsedByOtherApps, ownerUserId, loaderIsa);
120                    DexUseInfo existingData = packageUseInfo.mDexUseInfoMap.get(dexPath);
121                    if (existingData == null) {
122                        // It's the first time we see this dex file.
123                        packageUseInfo.mDexUseInfoMap.put(dexPath, newData);
124                        return true;
125                    } else {
126                        if (ownerUserId != existingData.mOwnerUserId) {
127                            // Oups, this should never happen, the DexManager who calls this should
128                            // do the proper checks and not call record if the user does not own the
129                            // dex path.
130                            // Secondary dex files are stored in the app user directory. A change in
131                            // owningUser for the same path means that something went wrong at some
132                            // higher level, and the loaderUser was allowed to cross
133                            // user-boundaries and access data from what we know to be the owner
134                            // user.
135                            throw new IllegalArgumentException("Trying to change ownerUserId for "
136                                    + " dex path " + dexPath + " from " + existingData.mOwnerUserId
137                                    + " to " + ownerUserId);
138                        }
139                        // Merge the information into the existing data.
140                        // Returns true if there was an update.
141                        return existingData.merge(newData);
142                    }
143                }
144            }
145        }
146    }
147
148    /**
149     * Convenience method for sync reads which does not force the user to pass a useless
150     * (Void) null.
151     */
152    public void read() {
153      read((Void) null);
154    }
155
156    /**
157     * Convenience method for async writes which does not force the user to pass a useless
158     * (Void) null.
159     */
160    public void maybeWriteAsync() {
161      maybeWriteAsync((Void) null);
162    }
163
164    @Override
165    protected void writeInternal(Void data) {
166        AtomicFile file = getFile();
167        FileOutputStream f = null;
168
169        try {
170            f = file.startWrite();
171            OutputStreamWriter osw = new OutputStreamWriter(f);
172            write(osw);
173            osw.flush();
174            file.finishWrite(f);
175        } catch (IOException e) {
176            if (f != null) {
177                file.failWrite(f);
178            }
179            Slog.e(TAG, "Failed to write usage for dex files", e);
180        }
181    }
182
183    /**
184     * File format:
185     *
186     * file_magic_version
187     * package_name_1
188     * #dex_file_path_1_1
189     * user_1_1, used_by_other_app_1_1, user_isa_1_1_1, user_isa_1_1_2
190     * #dex_file_path_1_2
191     * user_1_2, used_by_other_app_1_2, user_isa_1_2_1, user_isa_1_2_2
192     * ...
193     * package_name_2
194     * #dex_file_path_2_1
195     * user_2_1, used_by_other_app_2_1, user_isa_2_1_1, user_isa_2_1_2
196     * #dex_file_path_2_2,
197     * user_2_2, used_by_other_app_2_2, user_isa_2_2_1, user_isa_2_2_2
198     * ...
199    */
200    /* package */ void write(Writer out) {
201        // Make a clone to avoid locking while writing to disk.
202        Map<String, PackageUseInfo> packageUseInfoMapClone = clonePackageUseInfoMap();
203
204        FastPrintWriter fpw = new FastPrintWriter(out);
205
206        // Write the header.
207        fpw.print(PACKAGE_DEX_USAGE_VERSION_HEADER);
208        fpw.println(PACKAGE_DEX_USAGE_VERSION);
209
210        for (Map.Entry<String, PackageUseInfo> pEntry : packageUseInfoMapClone.entrySet()) {
211            // Write the package line.
212            String packageName = pEntry.getKey();
213            PackageUseInfo packageUseInfo = pEntry.getValue();
214
215            fpw.println(String.join(SPLIT_CHAR, packageName,
216                    writeBoolean(packageUseInfo.mIsUsedByOtherApps)));
217
218            // Write dex file lines.
219            for (Map.Entry<String, DexUseInfo> dEntry : packageUseInfo.mDexUseInfoMap.entrySet()) {
220                String dexPath = dEntry.getKey();
221                DexUseInfo dexUseInfo = dEntry.getValue();
222                fpw.println(DEX_LINE_CHAR + dexPath);
223                fpw.print(String.join(SPLIT_CHAR, Integer.toString(dexUseInfo.mOwnerUserId),
224                        writeBoolean(dexUseInfo.mIsUsedByOtherApps)));
225                for (String isa : dexUseInfo.mLoaderIsas) {
226                    fpw.print(SPLIT_CHAR + isa);
227                }
228                fpw.println();
229            }
230        }
231        fpw.flush();
232    }
233
234    @Override
235    protected void readInternal(Void data) {
236        AtomicFile file = getFile();
237        BufferedReader in = null;
238        try {
239            in = new BufferedReader(new InputStreamReader(file.openRead()));
240            read(in);
241        } catch (FileNotFoundException expected) {
242            // The file may not be there. E.g. When we first take the OTA with this feature.
243        } catch (IOException e) {
244            Slog.w(TAG, "Failed to parse package dex usage.", e);
245        } finally {
246            IoUtils.closeQuietly(in);
247        }
248    }
249
250    /* package */ void read(Reader reader) throws IOException {
251        Map<String, PackageUseInfo> data = new HashMap<>();
252        BufferedReader in = new BufferedReader(reader);
253        // Read header, do version check.
254        String versionLine = in.readLine();
255        if (versionLine == null) {
256            throw new IllegalStateException("No version line found.");
257        } else {
258            if (!versionLine.startsWith(PACKAGE_DEX_USAGE_VERSION_HEADER)) {
259                // TODO(calin): the caller is responsible to clear the file.
260                throw new IllegalStateException("Invalid version line: " + versionLine);
261            }
262            int version = Integer.parseInt(
263                    versionLine.substring(PACKAGE_DEX_USAGE_VERSION_HEADER.length()));
264            if (version != PACKAGE_DEX_USAGE_VERSION) {
265                throw new IllegalStateException("Unexpected version: " + version);
266            }
267        }
268
269        String s = null;
270        String currentPakage = null;
271        PackageUseInfo currentPakageData = null;
272
273        Set<String> supportedIsas = new HashSet<>();
274        for (String abi : Build.SUPPORTED_ABIS) {
275            supportedIsas.add(VMRuntime.getInstructionSet(abi));
276        }
277        while ((s = in.readLine()) != null) {
278            if (s.startsWith(DEX_LINE_CHAR)) {
279                // This is the start of the the dex lines.
280                // We expect two lines for each dex entry:
281                // #dexPaths
282                // onwerUserId,isUsedByOtherApps,isa1,isa2
283                if (currentPakage == null) {
284                    throw new IllegalStateException(
285                        "Malformed PackageDexUsage file. Expected package line before dex line.");
286                }
287
288                // First line is the dex path.
289                String dexPath = s.substring(DEX_LINE_CHAR.length());
290                // Next line is the dex data.
291                s = in.readLine();
292                if (s == null) {
293                    throw new IllegalStateException("Could not fine dexUseInfo for line: " + s);
294                }
295
296                // We expect at least 3 elements (isUsedByOtherApps, userId, isa).
297                String[] elems = s.split(SPLIT_CHAR);
298                if (elems.length < 3) {
299                    throw new IllegalStateException("Invalid PackageDexUsage line: " + s);
300                }
301                int ownerUserId = Integer.parseInt(elems[0]);
302                boolean isUsedByOtherApps = readBoolean(elems[1]);
303                DexUseInfo dexUseInfo = new DexUseInfo(isUsedByOtherApps, ownerUserId);
304                for (int i = 2; i < elems.length; i++) {
305                    String isa = elems[i];
306                    if (supportedIsas.contains(isa)) {
307                        dexUseInfo.mLoaderIsas.add(elems[i]);
308                    } else {
309                        // Should never happen unless someone crafts the file manually.
310                        // In theory it could if we drop a supported ISA after an OTA but we don't
311                        // do that.
312                        Slog.wtf(TAG, "Unsupported ISA when parsing PackageDexUsage: " + isa);
313                    }
314                }
315                if (supportedIsas.isEmpty()) {
316                    Slog.wtf(TAG, "Ignore dexPath when parsing PackageDexUsage because of " +
317                            "unsupported isas. dexPath=" + dexPath);
318                    continue;
319                }
320                currentPakageData.mDexUseInfoMap.put(dexPath, dexUseInfo);
321            } else {
322                // This is a package line.
323                // We expect it to be: `packageName,isUsedByOtherApps`.
324                String[] elems = s.split(SPLIT_CHAR);
325                if (elems.length != 2) {
326                    throw new IllegalStateException("Invalid PackageDexUsage line: " + s);
327                }
328                currentPakage = elems[0];
329                currentPakageData = new PackageUseInfo();
330                currentPakageData.mIsUsedByOtherApps = readBoolean(elems[1]);
331                data.put(currentPakage, currentPakageData);
332            }
333        }
334
335        synchronized (mPackageUseInfoMap) {
336            mPackageUseInfoMap.clear();
337            mPackageUseInfoMap.putAll(data);
338        }
339    }
340
341    /**
342     * Syncs the existing data with the set of available packages by removing obsolete entries.
343     */
344    public void syncData(Map<String, Set<Integer>> packageToUsersMap) {
345        synchronized (mPackageUseInfoMap) {
346            Iterator<Map.Entry<String, PackageUseInfo>> pIt =
347                    mPackageUseInfoMap.entrySet().iterator();
348            while (pIt.hasNext()) {
349                Map.Entry<String, PackageUseInfo> pEntry = pIt.next();
350                String packageName = pEntry.getKey();
351                PackageUseInfo packageUseInfo = pEntry.getValue();
352                Set<Integer> users = packageToUsersMap.get(packageName);
353                if (users == null) {
354                    // The package doesn't exist anymore, remove the record.
355                    pIt.remove();
356                } else {
357                    // The package exists but we can prune the entries associated with non existing
358                    // users.
359                    Iterator<Map.Entry<String, DexUseInfo>> dIt =
360                            packageUseInfo.mDexUseInfoMap.entrySet().iterator();
361                    while (dIt.hasNext()) {
362                        DexUseInfo dexUseInfo = dIt.next().getValue();
363                        if (!users.contains(dexUseInfo.mOwnerUserId)) {
364                            // User was probably removed. Delete its dex usage info.
365                            dIt.remove();
366                        }
367                    }
368                    if (!packageUseInfo.mIsUsedByOtherApps
369                            && packageUseInfo.mDexUseInfoMap.isEmpty()) {
370                        // The package is not used by other apps and we removed all its dex files
371                        // records. Remove the entire package record as well.
372                        pIt.remove();
373                    }
374                }
375            }
376        }
377    }
378
379    /**
380     * Clears the {@code usesByOtherApps} marker for the package {@code packageName}.
381     * @return true if the package usage info was updated.
382     */
383    public boolean clearUsedByOtherApps(String packageName) {
384        synchronized (mPackageUseInfoMap) {
385            PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(packageName);
386            if (packageUseInfo == null || !packageUseInfo.mIsUsedByOtherApps) {
387                return false;
388            }
389            packageUseInfo.mIsUsedByOtherApps = false;
390            return true;
391        }
392    }
393
394    /**
395     * Remove the usage data associated with package {@code packageName}.
396     * @return true if the package usage was found and removed successfully.
397     */
398    public boolean removePackage(String packageName) {
399        synchronized (mPackageUseInfoMap) {
400            return mPackageUseInfoMap.remove(packageName) != null;
401        }
402    }
403
404    /**
405     * Remove all the records about package {@code packageName} belonging to user {@code userId}.
406     * If the package is left with no records of secondary dex usage and is not used by other
407     * apps it will be removed as well.
408     * @return true if the record was found and actually deleted,
409     *         false if the record doesn't exist
410     */
411    public boolean removeUserPackage(String packageName, int userId) {
412        synchronized (mPackageUseInfoMap) {
413            PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(packageName);
414            if (packageUseInfo == null) {
415                return false;
416            }
417            boolean updated = false;
418            Iterator<Map.Entry<String, DexUseInfo>> dIt =
419                            packageUseInfo.mDexUseInfoMap.entrySet().iterator();
420            while (dIt.hasNext()) {
421                DexUseInfo dexUseInfo = dIt.next().getValue();
422                if (dexUseInfo.mOwnerUserId == userId) {
423                    dIt.remove();
424                    updated = true;
425                }
426            }
427            // If no secondary dex info is left and the package is not used by other apps
428            // remove the data since it is now useless.
429            if (packageUseInfo.mDexUseInfoMap.isEmpty() && !packageUseInfo.mIsUsedByOtherApps) {
430                mPackageUseInfoMap.remove(packageName);
431                updated = true;
432            }
433            return updated;
434        }
435    }
436
437    /**
438     * Remove the secondary dex file record belonging to the package {@code packageName}
439     * and user {@code userId}.
440     * @return true if the record was found and actually deleted,
441     *         false if the record doesn't exist
442     */
443    public boolean removeDexFile(String packageName, String dexFile, int userId) {
444        synchronized (mPackageUseInfoMap) {
445            PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(packageName);
446            if (packageUseInfo == null) {
447                return false;
448            }
449            return removeDexFile(packageUseInfo, dexFile, userId);
450        }
451    }
452
453    private boolean removeDexFile(PackageUseInfo packageUseInfo, String dexFile, int userId) {
454        DexUseInfo dexUseInfo = packageUseInfo.mDexUseInfoMap.get(dexFile);
455        if (dexUseInfo == null) {
456            return false;
457        }
458        if (dexUseInfo.mOwnerUserId == userId) {
459            packageUseInfo.mDexUseInfoMap.remove(dexFile);
460            return true;
461        }
462        return false;
463    }
464
465    public PackageUseInfo getPackageUseInfo(String packageName) {
466        synchronized (mPackageUseInfoMap) {
467            PackageUseInfo useInfo = mPackageUseInfoMap.get(packageName);
468            // The useInfo contains a map for secondary dex files which could be modified
469            // concurrently after this method returns and thus outside the locking we do here.
470            // (i.e. the map is updated when new class loaders are created, which can happen anytime
471            // after this method returns)
472            // Make a defensive copy to be sure we don't get concurrent modifications.
473            return useInfo == null ? null : new PackageUseInfo(useInfo);
474        }
475    }
476
477    /**
478     * Return all packages that contain records of secondary dex files.
479     */
480    public Set<String> getAllPackagesWithSecondaryDexFiles() {
481        Set<String> packages = new HashSet<>();
482        synchronized (mPackageUseInfoMap) {
483            for (Map.Entry<String, PackageUseInfo> entry : mPackageUseInfoMap.entrySet()) {
484                if (!entry.getValue().mDexUseInfoMap.isEmpty()) {
485                    packages.add(entry.getKey());
486                }
487            }
488        }
489        return packages;
490    }
491
492    public void clear() {
493        synchronized (mPackageUseInfoMap) {
494            mPackageUseInfoMap.clear();
495        }
496    }
497    // Creates a deep copy of the class' mPackageUseInfoMap.
498    private Map<String, PackageUseInfo> clonePackageUseInfoMap() {
499        Map<String, PackageUseInfo> clone = new HashMap<>();
500        synchronized (mPackageUseInfoMap) {
501            for (Map.Entry<String, PackageUseInfo> e : mPackageUseInfoMap.entrySet()) {
502                clone.put(e.getKey(), new PackageUseInfo(e.getValue()));
503            }
504        }
505        return clone;
506    }
507
508    private String writeBoolean(boolean bool) {
509        return bool ? "1" : "0";
510    }
511
512    private boolean readBoolean(String bool) {
513        if ("0".equals(bool)) return false;
514        if ("1".equals(bool)) return true;
515        throw new IllegalArgumentException("Unknown bool encoding: " + bool);
516    }
517
518    private boolean contains(int[] array, int elem) {
519        for (int i = 0; i < array.length; i++) {
520            if (elem == array[i]) {
521                return true;
522            }
523        }
524        return false;
525    }
526
527    public String dump() {
528        StringWriter sw = new StringWriter();
529        write(sw);
530        return sw.toString();
531    }
532
533    /**
534     * Stores data on how a package and its dex files are used.
535     */
536    public static class PackageUseInfo {
537        // This flag is for the primary and split apks. It is set to true whenever one of them
538        // is loaded by another app.
539        private boolean mIsUsedByOtherApps;
540        // Map dex paths to their data (isUsedByOtherApps, owner id, loader isa).
541        private final Map<String, DexUseInfo> mDexUseInfoMap;
542
543        public PackageUseInfo() {
544            mIsUsedByOtherApps = false;
545            mDexUseInfoMap = new HashMap<>();
546        }
547
548        // Creates a deep copy of the `other`.
549        public PackageUseInfo(PackageUseInfo other) {
550            mIsUsedByOtherApps = other.mIsUsedByOtherApps;
551            mDexUseInfoMap = new HashMap<>();
552            for (Map.Entry<String, DexUseInfo> e : other.mDexUseInfoMap.entrySet()) {
553                mDexUseInfoMap.put(e.getKey(), new DexUseInfo(e.getValue()));
554            }
555        }
556
557        private boolean merge(boolean isUsedByOtherApps) {
558            boolean oldIsUsedByOtherApps = mIsUsedByOtherApps;
559            mIsUsedByOtherApps = mIsUsedByOtherApps || isUsedByOtherApps;
560            return oldIsUsedByOtherApps != this.mIsUsedByOtherApps;
561        }
562
563        public boolean isUsedByOtherApps() {
564            return mIsUsedByOtherApps;
565        }
566
567        public Map<String, DexUseInfo> getDexUseInfoMap() {
568            return mDexUseInfoMap;
569        }
570    }
571
572    /**
573     * Stores data about a loaded dex files.
574     */
575    public static class DexUseInfo {
576        private boolean mIsUsedByOtherApps;
577        private final int mOwnerUserId;
578        private final Set<String> mLoaderIsas;
579
580        public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId) {
581            this(isUsedByOtherApps, ownerUserId, null);
582        }
583
584        public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId, String loaderIsa) {
585            mIsUsedByOtherApps = isUsedByOtherApps;
586            mOwnerUserId = ownerUserId;
587            mLoaderIsas = new HashSet<>();
588            if (loaderIsa != null) {
589                mLoaderIsas.add(loaderIsa);
590            }
591        }
592
593        // Creates a deep copy of the `other`.
594        public DexUseInfo(DexUseInfo other) {
595            mIsUsedByOtherApps = other.mIsUsedByOtherApps;
596            mOwnerUserId = other.mOwnerUserId;
597            mLoaderIsas = new HashSet<>(other.mLoaderIsas);
598        }
599
600        private boolean merge(DexUseInfo dexUseInfo) {
601            boolean oldIsUsedByOtherApps = mIsUsedByOtherApps;
602            mIsUsedByOtherApps = mIsUsedByOtherApps || dexUseInfo.mIsUsedByOtherApps;
603            boolean updateIsas = mLoaderIsas.addAll(dexUseInfo.mLoaderIsas);
604            return updateIsas || (oldIsUsedByOtherApps != mIsUsedByOtherApps);
605        }
606
607        public boolean isUsedByOtherApps() {
608            return mIsUsedByOtherApps;
609        }
610
611        public int getOwnerUserId() {
612            return mOwnerUserId;
613        }
614
615        public Set<String> getLoaderIsas() {
616            return mLoaderIsas;
617        }
618    }
619}
620