UsageStatsDatabase.java revision 7f61e96db7c90c1f4418359672aa4656aebee500
1/**
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16
17package com.android.server.usage;
18
19import android.app.usage.TimeSparseArray;
20import android.app.usage.UsageStatsManager;
21import android.util.AtomicFile;
22import android.util.Slog;
23
24import java.io.File;
25import java.io.FilenameFilter;
26import java.io.IOException;
27import java.util.ArrayList;
28import java.util.Calendar;
29import java.util.List;
30
31/**
32 * Provides an interface to query for UsageStat data from an XML database.
33 */
34class UsageStatsDatabase {
35    private static final String TAG = "UsageStatsDatabase";
36    private static final boolean DEBUG = UsageStatsService.DEBUG;
37
38    private final Object mLock = new Object();
39    private final File[] mIntervalDirs;
40    private final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
41    private final Calendar mCal;
42
43    public UsageStatsDatabase(File dir) {
44        mIntervalDirs = new File[] {
45                new File(dir, "daily"),
46                new File(dir, "weekly"),
47                new File(dir, "monthly"),
48                new File(dir, "yearly"),
49        };
50        mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
51        mCal = Calendar.getInstance();
52    }
53
54    /**
55     * Initialize any directories required and index what stats are available.
56     */
57    void init() {
58        synchronized (mLock) {
59            for (File f : mIntervalDirs) {
60                f.mkdirs();
61                if (!f.exists()) {
62                    throw new IllegalStateException("Failed to create directory "
63                            + f.getAbsolutePath());
64                }
65            }
66
67            final FilenameFilter backupFileFilter = new FilenameFilter() {
68                @Override
69                public boolean accept(File dir, String name) {
70                    return !name.endsWith(".bak");
71                }
72            };
73
74            // Index the available usage stat files on disk.
75            for (int i = 0; i < mSortedStatFiles.length; i++) {
76                mSortedStatFiles[i] = new TimeSparseArray<>();
77                File[] files = mIntervalDirs[i].listFiles(backupFileFilter);
78                if (files != null) {
79                    if (DEBUG) {
80                        Slog.d(TAG, "Found " + files.length + " stat files for interval " + i);
81                    }
82
83                    for (File f : files) {
84                        mSortedStatFiles[i].put(Long.parseLong(f.getName()), new AtomicFile(f));
85                    }
86                }
87            }
88        }
89    }
90
91    /**
92     * Get the latest stats that exist for this interval type.
93     */
94    public IntervalStats getLatestUsageStats(int intervalType) {
95        synchronized (mLock) {
96            if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
97                throw new IllegalArgumentException("Bad interval type " + intervalType);
98            }
99
100            final int fileCount = mSortedStatFiles[intervalType].size();
101            if (fileCount == 0) {
102                return null;
103            }
104
105            try {
106                final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1);
107                IntervalStats stats = new IntervalStats();
108                UsageStatsXml.read(f, stats);
109                return stats;
110            } catch (IOException e) {
111                Slog.e(TAG, "Failed to read usage stats file", e);
112            }
113        }
114        return null;
115    }
116
117    /**
118     * Get the time at which the latest stats begin for this interval type.
119     */
120    public long getLatestUsageStatsBeginTime(int intervalType) {
121        synchronized (mLock) {
122            if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
123                throw new IllegalArgumentException("Bad interval type " + intervalType);
124            }
125
126            final int statsFileCount = mSortedStatFiles[intervalType].size();
127            if (statsFileCount > 0) {
128                return mSortedStatFiles[intervalType].keyAt(statsFileCount - 1);
129            }
130            return -1;
131        }
132    }
133
134    /**
135     * Figures out what to extract from the given IntervalStats object.
136     */
137    interface StatCombiner<T> {
138
139        /**
140         * Implementations should extract interesting from <code>stats</code> and add it
141         * to the <code>accumulatedResult</code> list.
142         *
143         * If the <code>stats</code> object is mutable, <code>mutable</code> will be true,
144         * which means you should make a copy of the data before adding it to the
145         * <code>accumulatedResult</code> list.
146         *
147         * @param stats The {@link IntervalStats} object selected.
148         * @param mutable Whether or not the data inside the stats object is mutable.
149         * @param accumulatedResult The list to which to add extracted data.
150         */
151        void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult);
152    }
153
154    /**
155     * Find all {@link IntervalStats} for the given range and interval type.
156     */
157    public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime,
158            StatCombiner<T> combiner) {
159        synchronized (mLock) {
160            if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
161                throw new IllegalArgumentException("Bad interval type " + intervalType);
162            }
163
164            if (endTime < beginTime) {
165                return null;
166            }
167
168            final int startIndex = mSortedStatFiles[intervalType].closestIndexOnOrBefore(beginTime);
169            if (startIndex < 0) {
170                return null;
171            }
172
173            int endIndex = mSortedStatFiles[intervalType].closestIndexOnOrAfter(endTime);
174            if (endIndex < 0) {
175                endIndex = mSortedStatFiles[intervalType].size() - 1;
176            }
177
178            try {
179                IntervalStats stats = new IntervalStats();
180                ArrayList<T> results = new ArrayList<>();
181                for (int i = startIndex; i <= endIndex; i++) {
182                    final AtomicFile f = mSortedStatFiles[intervalType].valueAt(i);
183
184                    if (DEBUG) {
185                        Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
186                    }
187
188                    UsageStatsXml.read(f, stats);
189                    if (beginTime < stats.endTime) {
190                        combiner.combine(stats, false, results);
191                    }
192                }
193                return results;
194            } catch (IOException e) {
195                Slog.e(TAG, "Failed to read usage stats file", e);
196                return null;
197            }
198        }
199    }
200
201    /**
202     * Find the interval that best matches this range.
203     *
204     * TODO(adamlesinski): Use endTimeStamp in best fit calculation.
205     */
206    public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) {
207        synchronized (mLock) {
208            int bestBucket = -1;
209            long smallestDiff = Long.MAX_VALUE;
210            for (int i = mSortedStatFiles.length - 1; i >= 0; i--) {
211                final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp);
212                int size = mSortedStatFiles[i].size();
213                if (index >= 0 && index < size) {
214                    // We have some results here, check if they are better than our current match.
215                    long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp);
216                    if (diff < smallestDiff) {
217                        smallestDiff = diff;
218                        bestBucket = i;
219                    }
220                }
221            }
222            return bestBucket;
223        }
224    }
225
226    /**
227     * Remove any usage stat files that are too old.
228     */
229    public void prune() {
230        synchronized (mLock) {
231            long timeNow = System.currentTimeMillis();
232            mCal.setTimeInMillis(timeNow);
233            mCal.add(Calendar.YEAR, -3);
234            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY],
235                    mCal.getTimeInMillis());
236
237            mCal.setTimeInMillis(timeNow);
238            mCal.add(Calendar.MONTH, -6);
239            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
240                    mCal.getTimeInMillis());
241
242            mCal.setTimeInMillis(timeNow);
243            mCal.add(Calendar.WEEK_OF_YEAR, -4);
244            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
245                    mCal.getTimeInMillis());
246
247            mCal.setTimeInMillis(timeNow);
248            mCal.add(Calendar.DAY_OF_YEAR, -7);
249            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
250                    mCal.getTimeInMillis());
251        }
252    }
253
254    private static void pruneFilesOlderThan(File dir, long expiryTime) {
255        File[] files = dir.listFiles();
256        if (files != null) {
257            for (File f : files) {
258                long beginTime = Long.parseLong(f.getName());
259                if (beginTime < expiryTime) {
260                    new AtomicFile(f).delete();
261                }
262            }
263        }
264    }
265
266    /**
267     * Update the stats in the database. They may not be written to disk immediately.
268     */
269    public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
270        synchronized (mLock) {
271            if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
272                throw new IllegalArgumentException("Bad interval type " + intervalType);
273            }
274
275            AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
276            if (f == null) {
277                f = new AtomicFile(new File(mIntervalDirs[intervalType],
278                        Long.toString(stats.beginTime)));
279                mSortedStatFiles[intervalType].put(stats.beginTime, f);
280            }
281
282            UsageStatsXml.write(f, stats);
283            stats.lastTimeSaved = f.getLastModifiedTime();
284        }
285    }
286}
287