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;
18
19import static android.os.Process.PACKAGE_INFO_GID;
20import static android.os.Process.SYSTEM_UID;
21
22import android.content.pm.PackageManager;
23import android.content.pm.PackageParser;
24import android.os.FileUtils;
25import android.util.AtomicFile;
26import android.util.Log;
27
28import libcore.io.IoUtils;
29
30import java.io.BufferedInputStream;
31import java.io.BufferedOutputStream;
32import java.io.FileNotFoundException;
33import java.io.FileOutputStream;
34import java.io.IOException;
35import java.io.InputStream;
36import java.nio.charset.StandardCharsets;
37import java.util.Map;
38
39class PackageUsage extends AbstractStatsBase<Map<String, PackageParser.Package>> {
40
41    private static final String USAGE_FILE_MAGIC = "PACKAGE_USAGE__VERSION_";
42    private static final String USAGE_FILE_MAGIC_VERSION_1 = USAGE_FILE_MAGIC + "1";
43
44    private boolean mIsHistoricalPackageUsageAvailable = true;
45
46    PackageUsage() {
47        super("package-usage.list", "PackageUsage_DiskWriter", /* lock */ true);
48    }
49
50    boolean isHistoricalPackageUsageAvailable() {
51        return mIsHistoricalPackageUsageAvailable;
52    }
53
54    @Override
55    protected void writeInternal(Map<String, PackageParser.Package> packages) {
56        AtomicFile file = getFile();
57        FileOutputStream f = null;
58        try {
59            f = file.startWrite();
60            BufferedOutputStream out = new BufferedOutputStream(f);
61            FileUtils.setPermissions(file.getBaseFile().getPath(),
62                    0640, SYSTEM_UID, PACKAGE_INFO_GID);
63            StringBuilder sb = new StringBuilder();
64
65            sb.append(USAGE_FILE_MAGIC_VERSION_1);
66            sb.append('\n');
67            out.write(sb.toString().getBytes(StandardCharsets.US_ASCII));
68
69            for (PackageParser.Package pkg : packages.values()) {
70                if (pkg.getLatestPackageUseTimeInMills() == 0L) {
71                    continue;
72                }
73                sb.setLength(0);
74                sb.append(pkg.packageName);
75                for (long usageTimeInMillis : pkg.mLastPackageUsageTimeInMills) {
76                    sb.append(' ');
77                    sb.append(usageTimeInMillis);
78                }
79                sb.append('\n');
80                out.write(sb.toString().getBytes(StandardCharsets.US_ASCII));
81            }
82            out.flush();
83            file.finishWrite(f);
84        } catch (IOException e) {
85            if (f != null) {
86                file.failWrite(f);
87            }
88            Log.e(PackageManagerService.TAG, "Failed to write package usage times", e);
89        }
90    }
91
92    @Override
93    protected void readInternal(Map<String, PackageParser.Package> packages) {
94        AtomicFile file = getFile();
95        BufferedInputStream in = null;
96        try {
97            in = new BufferedInputStream(file.openRead());
98            StringBuffer sb = new StringBuffer();
99
100            String firstLine = readLine(in, sb);
101            if (firstLine == null) {
102                // Empty file. Do nothing.
103            } else if (USAGE_FILE_MAGIC_VERSION_1.equals(firstLine)) {
104                readVersion1LP(packages, in, sb);
105            } else {
106                readVersion0LP(packages, in, sb, firstLine);
107            }
108        } catch (FileNotFoundException expected) {
109            mIsHistoricalPackageUsageAvailable = false;
110        } catch (IOException e) {
111            Log.w(PackageManagerService.TAG, "Failed to read package usage times", e);
112        } finally {
113            IoUtils.closeQuietly(in);
114        }
115    }
116
117    private void readVersion0LP(Map<String, PackageParser.Package> packages, InputStream in,
118            StringBuffer sb, String firstLine)
119            throws IOException {
120        // Initial version of the file had no version number and stored one
121        // package-timestamp pair per line.
122        // Note that the first line has already been read from the InputStream.
123        for (String line = firstLine; line != null; line = readLine(in, sb)) {
124            String[] tokens = line.split(" ");
125            if (tokens.length != 2) {
126                throw new IOException("Failed to parse " + line +
127                        " as package-timestamp pair.");
128            }
129
130            String packageName = tokens[0];
131            PackageParser.Package pkg = packages.get(packageName);
132            if (pkg == null) {
133                continue;
134            }
135
136            long timestamp = parseAsLong(tokens[1]);
137            for (int reason = 0;
138                    reason < PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT;
139                    reason++) {
140                pkg.mLastPackageUsageTimeInMills[reason] = timestamp;
141            }
142        }
143    }
144
145    private void readVersion1LP(Map<String, PackageParser.Package> packages, InputStream in,
146            StringBuffer sb) throws IOException {
147        // Version 1 of the file started with the corresponding version
148        // number and then stored a package name and eight timestamps per line.
149        String line;
150        while ((line = readLine(in, sb)) != null) {
151            String[] tokens = line.split(" ");
152            if (tokens.length != PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT + 1) {
153                throw new IOException("Failed to parse " + line + " as a timestamp array.");
154            }
155
156            String packageName = tokens[0];
157            PackageParser.Package pkg = packages.get(packageName);
158            if (pkg == null) {
159                continue;
160            }
161
162            for (int reason = 0;
163                    reason < PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT;
164                    reason++) {
165                pkg.mLastPackageUsageTimeInMills[reason] = parseAsLong(tokens[reason + 1]);
166            }
167        }
168    }
169
170    private long parseAsLong(String token) throws IOException {
171        try {
172            return Long.parseLong(token);
173        } catch (NumberFormatException e) {
174            throw new IOException("Failed to parse " + token + " as a long.", e);
175        }
176    }
177
178    private String readLine(InputStream in, StringBuffer sb) throws IOException {
179        return readToken(in, sb, '\n');
180    }
181
182    private String readToken(InputStream in, StringBuffer sb, char endOfToken)
183            throws IOException {
184        sb.setLength(0);
185        while (true) {
186            int ch = in.read();
187            if (ch == -1) {
188                if (sb.length() == 0) {
189                    return null;
190                }
191                throw new IOException("Unexpected EOF");
192            }
193            if (ch == endOfToken) {
194                return sb.toString();
195            }
196            sb.append((char)ch);
197        }
198    }
199}