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 
17 package com.android.server.pm;
18 
19 import static android.os.Process.PACKAGE_INFO_GID;
20 import static android.os.Process.SYSTEM_UID;
21 
22 import android.content.pm.PackageManager;
23 import android.content.pm.PackageParser;
24 import android.os.FileUtils;
25 import android.util.AtomicFile;
26 import android.util.Log;
27 
28 import libcore.io.IoUtils;
29 
30 import java.io.BufferedInputStream;
31 import java.io.BufferedOutputStream;
32 import java.io.FileNotFoundException;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.nio.charset.StandardCharsets;
37 import java.util.Map;
38 
39 class 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 
PackageUsage()46     PackageUsage() {
47         super("package-usage.list", "PackageUsage_DiskWriter", /* lock */ true);
48     }
49 
isHistoricalPackageUsageAvailable()50     boolean isHistoricalPackageUsageAvailable() {
51         return mIsHistoricalPackageUsageAvailable;
52     }
53 
54     @Override
writeInternal(Map<String, PackageParser.Package> packages)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
readInternal(Map<String, PackageParser.Package> packages)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 
readVersion0LP(Map<String, PackageParser.Package> packages, InputStream in, StringBuffer sb, String firstLine)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 
readVersion1LP(Map<String, PackageParser.Package> packages, InputStream in, StringBuffer sb)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 
parseAsLong(String token)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 
readLine(InputStream in, StringBuffer sb)178     private String readLine(InputStream in, StringBuffer sb) throws IOException {
179         return readToken(in, sb, '\n');
180     }
181 
readToken(InputStream in, StringBuffer sb, char endOfToken)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 }