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 
17 package com.android.server.usage;
18 
19 import android.app.usage.TimeSparseArray;
20 import android.app.usage.UsageStats;
21 import android.app.usage.UsageStatsManager;
22 import android.os.Build;
23 import android.os.SystemProperties;
24 import android.util.AtomicFile;
25 import android.util.Slog;
26 import android.util.TimeUtils;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.internal.util.IndentingPrintWriter;
30 
31 import libcore.io.IoUtils;
32 
33 import java.io.BufferedReader;
34 import java.io.BufferedWriter;
35 import java.io.ByteArrayInputStream;
36 import java.io.ByteArrayOutputStream;
37 import java.io.DataInputStream;
38 import java.io.DataOutputStream;
39 import java.io.File;
40 import java.io.FileInputStream;
41 import java.io.FileNotFoundException;
42 import java.io.FileOutputStream;
43 import java.io.FileReader;
44 import java.io.FileWriter;
45 import java.io.FilenameFilter;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
49 import java.nio.file.Files;
50 import java.nio.file.StandardCopyOption;
51 import java.util.ArrayList;
52 import java.util.List;
53 
54 /**
55  * Provides an interface to query for UsageStat data from a Protocol Buffer database.
56  *
57  * Prior to version 4, UsageStatsDatabase used XML to store Usage Stats data to disk.
58  * When the UsageStatsDatabase version is upgraded, the files on disk are migrated to the new
59  * version on init. The steps of migration are as follows:
60  * 1) Check if version upgrade breadcrumb exists on disk, if so skip to step 4.
61  * 2) Move current files to a timestamped backup directory.
62  * 3) Write a temporary breadcrumb file with some info about the backup directory.
63  * 4) Deserialize the backup files in the timestamped backup folder referenced by the breadcrumb.
64  * 5) Reserialize the data read from the file with the new version format and replace the old files
65  * 6) Repeat Step 3 and 4 for each file in the backup folder.
66  * 7) Update the version file with the new version and build fingerprint.
67  * 8) Delete the time stamped backup folder (unless flagged to be kept).
68  * 9) Delete the breadcrumb file.
69  *
70  * Performing the upgrade steps in this order, protects against unexpected shutdowns mid upgrade
71  *
72  * The backup directory will contain directories with timestamp names. If the upgrade breadcrumb
73  * exists on disk, it will contain a timestamp which will match one of the backup directories. The
74  * breadcrumb will also contain a version number which will denote how the files in the backup
75  * directory should be deserialized.
76  */
77 public class UsageStatsDatabase {
78     private static final int DEFAULT_CURRENT_VERSION = 4;
79     /**
80      * Current version of the backup schema
81      *
82      * @hide
83      */
84     @VisibleForTesting
85     public static final int BACKUP_VERSION = 4;
86 
87     @VisibleForTesting
88     static final int[] MAX_FILES_PER_INTERVAL_TYPE = new int[]{100, 50, 12, 10};
89 
90     // Key under which the payload blob is stored
91     // same as UsageStatsBackupHelper.KEY_USAGE_STATS
92     static final String KEY_USAGE_STATS = "usage_stats";
93 
94     // Persist versioned backup files.
95     // Should be false, except when testing new versions
96     static final boolean KEEP_BACKUP_DIR = false;
97 
98     private static final String TAG = "UsageStatsDatabase";
99     private static final boolean DEBUG = UsageStatsService.DEBUG;
100     private static final String BAK_SUFFIX = ".bak";
101     private static final String CHECKED_IN_SUFFIX = UsageStatsXml.CHECKED_IN_SUFFIX;
102     private static final String RETENTION_LEN_KEY = "ro.usagestats.chooser.retention";
103     private static final int SELECTION_LOG_RETENTION_LEN =
104             SystemProperties.getInt(RETENTION_LEN_KEY, 14);
105 
106     private final Object mLock = new Object();
107     private final File[] mIntervalDirs;
108     @VisibleForTesting
109     final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
110     private final UnixCalendar mCal;
111     private final File mVersionFile;
112     private final File mBackupsDir;
113     // If this file exists on disk, UsageStatsDatabase is in the middle of migrating files to a new
114     // version. If this file exists on boot, the upgrade was interrupted and needs to be picked up
115     // where it left off.
116     private final File mUpdateBreadcrumb;
117     // Current version of the database files schema
118     private int mCurrentVersion;
119     private boolean mFirstUpdate;
120     private boolean mNewUpdate;
121 
122     /**
123      * UsageStatsDatabase constructor that allows setting the version number.
124      * This should only be used for testing.
125      *
126      * @hide
127      */
128     @VisibleForTesting
UsageStatsDatabase(File dir, int version)129     public UsageStatsDatabase(File dir, int version) {
130         mIntervalDirs = new File[]{
131             new File(dir, "daily"),
132             new File(dir, "weekly"),
133             new File(dir, "monthly"),
134             new File(dir, "yearly"),
135         };
136         mCurrentVersion = version;
137         mVersionFile = new File(dir, "version");
138         mBackupsDir = new File(dir, "backups");
139         mUpdateBreadcrumb = new File(dir, "breadcrumb");
140         mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
141         mCal = new UnixCalendar(0);
142     }
143 
UsageStatsDatabase(File dir)144     public UsageStatsDatabase(File dir) {
145         this(dir, DEFAULT_CURRENT_VERSION);
146     }
147 
148     /**
149      * Initialize any directories required and index what stats are available.
150      */
init(long currentTimeMillis)151     public void init(long currentTimeMillis) {
152         synchronized (mLock) {
153             for (File f : mIntervalDirs) {
154                 f.mkdirs();
155                 if (!f.exists()) {
156                     throw new IllegalStateException("Failed to create directory "
157                             + f.getAbsolutePath());
158                 }
159             }
160 
161             checkVersionAndBuildLocked();
162             indexFilesLocked();
163 
164             // Delete files that are in the future.
165             for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) {
166                 final int startIndex = files.closestIndexOnOrAfter(currentTimeMillis);
167                 if (startIndex < 0) {
168                     continue;
169                 }
170 
171                 final int fileCount = files.size();
172                 for (int i = startIndex; i < fileCount; i++) {
173                     files.valueAt(i).delete();
174                 }
175 
176                 // Remove in a separate loop because any accesses (valueAt)
177                 // will cause a gc in the SparseArray and mess up the order.
178                 for (int i = startIndex; i < fileCount; i++) {
179                     files.removeAt(i);
180                 }
181             }
182         }
183     }
184 
185     public interface CheckinAction {
checkin(IntervalStats stats)186         boolean checkin(IntervalStats stats);
187     }
188 
189     /**
190      * Calls {@link CheckinAction#checkin(IntervalStats)} on the given {@link CheckinAction}
191      * for all {@link IntervalStats} that haven't been checked-in.
192      * If any of the calls to {@link CheckinAction#checkin(IntervalStats)} returns false or throws
193      * an exception, the check-in will be aborted.
194      *
195      * @param checkinAction The callback to run when checking-in {@link IntervalStats}.
196      * @return true if the check-in succeeded.
197      */
checkinDailyFiles(CheckinAction checkinAction)198     public boolean checkinDailyFiles(CheckinAction checkinAction) {
199         synchronized (mLock) {
200             final TimeSparseArray<AtomicFile> files =
201                     mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY];
202             final int fileCount = files.size();
203 
204             // We may have holes in the checkin (if there was an error)
205             // so find the last checked-in file and go from there.
206             int lastCheckin = -1;
207             for (int i = 0; i < fileCount - 1; i++) {
208                 if (files.valueAt(i).getBaseFile().getPath().endsWith(CHECKED_IN_SUFFIX)) {
209                     lastCheckin = i;
210                 }
211             }
212 
213             final int start = lastCheckin + 1;
214             if (start == fileCount - 1) {
215                 return true;
216             }
217 
218             try {
219                 IntervalStats stats = new IntervalStats();
220                 for (int i = start; i < fileCount - 1; i++) {
221                     readLocked(files.valueAt(i), stats);
222                     if (!checkinAction.checkin(stats)) {
223                         return false;
224                     }
225                 }
226             } catch (IOException e) {
227                 Slog.e(TAG, "Failed to check-in", e);
228                 return false;
229             }
230 
231             // We have successfully checked-in the stats, so rename the files so that they
232             // are marked as checked-in.
233             for (int i = start; i < fileCount - 1; i++) {
234                 final AtomicFile file = files.valueAt(i);
235                 final File checkedInFile = new File(
236                         file.getBaseFile().getPath() + CHECKED_IN_SUFFIX);
237                 if (!file.getBaseFile().renameTo(checkedInFile)) {
238                     // We must return success, as we've already marked some files as checked-in.
239                     // It's better to repeat ourselves than to lose data.
240                     Slog.e(TAG, "Failed to mark file " + file.getBaseFile().getPath()
241                             + " as checked-in");
242                     return true;
243                 }
244 
245                 // AtomicFile needs to set a new backup path with the same -c extension, so
246                 // we replace the old AtomicFile with the updated one.
247                 files.setValueAt(i, new AtomicFile(checkedInFile));
248             }
249         }
250         return true;
251     }
252 
253     /** @hide */
254     @VisibleForTesting
forceIndexFiles()255     void forceIndexFiles() {
256         synchronized (mLock) {
257             indexFilesLocked();
258         }
259     }
260 
indexFilesLocked()261     private void indexFilesLocked() {
262         final FilenameFilter backupFileFilter = new FilenameFilter() {
263             @Override
264             public boolean accept(File dir, String name) {
265                 return !name.endsWith(BAK_SUFFIX);
266             }
267         };
268         // Index the available usage stat files on disk.
269         for (int i = 0; i < mSortedStatFiles.length; i++) {
270             if (mSortedStatFiles[i] == null) {
271                 mSortedStatFiles[i] = new TimeSparseArray<>();
272             } else {
273                 mSortedStatFiles[i].clear();
274             }
275             File[] files = mIntervalDirs[i].listFiles(backupFileFilter);
276             if (files != null) {
277                 if (DEBUG) {
278                     Slog.d(TAG, "Found " + files.length + " stat files for interval " + i);
279                 }
280                 final int len = files.length;
281                 for (int j = 0; j < len; j++) {
282                     final File f = files[j];
283                     final AtomicFile af = new AtomicFile(f);
284                     try {
285                         mSortedStatFiles[i].put(parseBeginTime(af), af);
286                     } catch (IOException e) {
287                         Slog.e(TAG, "failed to index file: " + f, e);
288                     }
289                 }
290 
291                 // only keep the max allowed number of files for each interval type.
292                 final int toDelete = mSortedStatFiles[i].size() - MAX_FILES_PER_INTERVAL_TYPE[i];
293                 if (toDelete > 0) {
294                     for (int j = 0; j < toDelete; j++) {
295                         mSortedStatFiles[i].valueAt(0).delete();
296                         mSortedStatFiles[i].removeAt(0);
297                     }
298                     Slog.d(TAG, "Deleted " + toDelete + " stat files for interval " + i);
299                 }
300             }
301         }
302     }
303 
304     /**
305      * Is this the first update to the system from L to M?
306      */
isFirstUpdate()307     boolean isFirstUpdate() {
308         return mFirstUpdate;
309     }
310 
311     /**
312      * Is this a system update since we started tracking build fingerprint in the version file?
313      */
isNewUpdate()314     boolean isNewUpdate() {
315         return mNewUpdate;
316     }
317 
checkVersionAndBuildLocked()318     private void checkVersionAndBuildLocked() {
319         int version;
320         String buildFingerprint;
321         String currentFingerprint = getBuildFingerprint();
322         mFirstUpdate = true;
323         mNewUpdate = true;
324         try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
325             version = Integer.parseInt(reader.readLine());
326             buildFingerprint = reader.readLine();
327             if (buildFingerprint != null) {
328                 mFirstUpdate = false;
329             }
330             if (currentFingerprint.equals(buildFingerprint)) {
331                 mNewUpdate = false;
332             }
333         } catch (NumberFormatException | IOException e) {
334             version = 0;
335         }
336 
337         if (version != mCurrentVersion) {
338             Slog.i(TAG, "Upgrading from version " + version + " to " + mCurrentVersion);
339             if (!mUpdateBreadcrumb.exists()) {
340                 try {
341                     doUpgradeLocked(version);
342                 } catch (Exception e) {
343                     Slog.e(TAG,
344                             "Failed to upgrade from version " + version + " to " + mCurrentVersion,
345                             e);
346                     // Fallback to previous version.
347                     mCurrentVersion = version;
348                     return;
349                 }
350             } else {
351                 Slog.i(TAG, "Version upgrade breadcrumb found on disk! Continuing version upgrade");
352             }
353         }
354 
355         if (mUpdateBreadcrumb.exists()) {
356             int previousVersion;
357             long token;
358             try (BufferedReader reader = new BufferedReader(
359                     new FileReader(mUpdateBreadcrumb))) {
360                 token = Long.parseLong(reader.readLine());
361                 previousVersion = Integer.parseInt(reader.readLine());
362             } catch (NumberFormatException | IOException e) {
363                 Slog.e(TAG, "Failed read version upgrade breadcrumb");
364                 throw new RuntimeException(e);
365             }
366             continueUpgradeLocked(previousVersion, token);
367         }
368 
369         if (version != mCurrentVersion || mNewUpdate) {
370             try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
371                 writer.write(Integer.toString(mCurrentVersion));
372                 writer.write("\n");
373                 writer.write(currentFingerprint);
374                 writer.write("\n");
375                 writer.flush();
376             } catch (IOException e) {
377                 Slog.e(TAG, "Failed to write new version");
378                 throw new RuntimeException(e);
379             }
380         }
381 
382         if (mUpdateBreadcrumb.exists()) {
383             // Files should be up to date with current version. Clear the version update breadcrumb
384             mUpdateBreadcrumb.delete();
385         }
386 
387         if (mBackupsDir.exists() && !KEEP_BACKUP_DIR) {
388             deleteDirectory(mBackupsDir);
389         }
390     }
391 
getBuildFingerprint()392     private String getBuildFingerprint() {
393         return Build.VERSION.RELEASE + ";"
394                 + Build.VERSION.CODENAME + ";"
395                 + Build.VERSION.INCREMENTAL;
396     }
397 
doUpgradeLocked(int thisVersion)398     private void doUpgradeLocked(int thisVersion) {
399         if (thisVersion < 2) {
400             // Delete all files if we are version 0. This is a pre-release version,
401             // so this is fine.
402             Slog.i(TAG, "Deleting all usage stats files");
403             for (int i = 0; i < mIntervalDirs.length; i++) {
404                 File[] files = mIntervalDirs[i].listFiles();
405                 if (files != null) {
406                     for (File f : files) {
407                         f.delete();
408                     }
409                 }
410             }
411         } else {
412             // Create a dir in backups based on current timestamp
413             final long token = System.currentTimeMillis();
414             final File backupDir = new File(mBackupsDir, Long.toString(token));
415             backupDir.mkdirs();
416             if (!backupDir.exists()) {
417                 throw new IllegalStateException(
418                         "Failed to create backup directory " + backupDir.getAbsolutePath());
419             }
420             try {
421                 Files.copy(mVersionFile.toPath(),
422                         new File(backupDir, mVersionFile.getName()).toPath(),
423                         StandardCopyOption.REPLACE_EXISTING);
424             } catch (IOException e) {
425                 Slog.e(TAG, "Failed to back up version file : " + mVersionFile.toString());
426                 throw new RuntimeException(e);
427             }
428 
429             for (int i = 0; i < mIntervalDirs.length; i++) {
430                 final File backupIntervalDir = new File(backupDir, mIntervalDirs[i].getName());
431                 backupIntervalDir.mkdir();
432 
433                 if (!backupIntervalDir.exists()) {
434                     throw new IllegalStateException(
435                             "Failed to create interval backup directory "
436                                     + backupIntervalDir.getAbsolutePath());
437                 }
438                 File[] files = mIntervalDirs[i].listFiles();
439                 if (files != null) {
440                     for (int j = 0; j < files.length; j++) {
441                         final File backupFile = new File(backupIntervalDir, files[j].getName());
442                         if (DEBUG) {
443                             Slog.d(TAG, "Creating versioned (" + Integer.toString(thisVersion)
444                                     + ") backup of " + files[j].toString()
445                                     + " stat files for interval "
446                                     + i + " to " + backupFile.toString());
447                         }
448 
449                         try {
450                             // Backup file should not already exist, but make sure it doesn't
451                             Files.move(files[j].toPath(), backupFile.toPath(),
452                                     StandardCopyOption.REPLACE_EXISTING);
453                         } catch (IOException e) {
454                             Slog.e(TAG, "Failed to back up file : " + files[j].toString());
455                             throw new RuntimeException(e);
456                         }
457                     }
458                 }
459             }
460 
461             // Leave a breadcrumb behind noting that all the usage stats have been moved to a backup
462             BufferedWriter writer = null;
463             try {
464                 writer = new BufferedWriter(new FileWriter(mUpdateBreadcrumb));
465                 writer.write(Long.toString(token));
466                 writer.write("\n");
467                 writer.write(Integer.toString(thisVersion));
468                 writer.write("\n");
469                 writer.flush();
470             } catch (IOException e) {
471                 Slog.e(TAG, "Failed to write new version upgrade breadcrumb");
472                 throw new RuntimeException(e);
473             } finally {
474                 IoUtils.closeQuietly(writer);
475             }
476         }
477     }
478 
continueUpgradeLocked(int version, long token)479     private void continueUpgradeLocked(int version, long token) {
480         final File backupDir = new File(mBackupsDir, Long.toString(token));
481 
482         // Read each file in the backup according to the version and write to the interval
483         // directories in the current versions format
484         for (int i = 0; i < mIntervalDirs.length; i++) {
485             final File backedUpInterval = new File(backupDir, mIntervalDirs[i].getName());
486             File[] files = backedUpInterval.listFiles();
487             if (files != null) {
488                 for (int j = 0; j < files.length; j++) {
489                     if (DEBUG) {
490                         Slog.d(TAG,
491                                 "Upgrading " + files[j].toString() + " to version ("
492                                         + Integer.toString(
493                                         mCurrentVersion) + ") for interval " + i);
494                     }
495                     try {
496                         IntervalStats stats = new IntervalStats();
497                         readLocked(new AtomicFile(files[j]), stats, version);
498                         writeLocked(new AtomicFile(new File(mIntervalDirs[i],
499                                 Long.toString(stats.beginTime))), stats, mCurrentVersion);
500                     } catch (Exception e) {
501                         // This method is called on boot, log the exception and move on
502                         Slog.e(TAG, "Failed to upgrade backup file : " + files[j].toString());
503                     }
504                 }
505             }
506         }
507     }
508 
onTimeChanged(long timeDiffMillis)509     public void onTimeChanged(long timeDiffMillis) {
510         synchronized (mLock) {
511             StringBuilder logBuilder = new StringBuilder();
512             logBuilder.append("Time changed by ");
513             TimeUtils.formatDuration(timeDiffMillis, logBuilder);
514             logBuilder.append(".");
515 
516             int filesDeleted = 0;
517             int filesMoved = 0;
518 
519             for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) {
520                 final int fileCount = files.size();
521                 for (int i = 0; i < fileCount; i++) {
522                     final AtomicFile file = files.valueAt(i);
523                     final long newTime = files.keyAt(i) + timeDiffMillis;
524                     if (newTime < 0) {
525                         filesDeleted++;
526                         file.delete();
527                     } else {
528                         try {
529                             file.openRead().close();
530                         } catch (IOException e) {
531                             // Ignore, this is just to make sure there are no backups.
532                         }
533 
534                         String newName = Long.toString(newTime);
535                         if (file.getBaseFile().getName().endsWith(CHECKED_IN_SUFFIX)) {
536                             newName = newName + CHECKED_IN_SUFFIX;
537                         }
538 
539                         final File newFile = new File(file.getBaseFile().getParentFile(), newName);
540                         filesMoved++;
541                         file.getBaseFile().renameTo(newFile);
542                     }
543                 }
544                 files.clear();
545             }
546 
547             logBuilder.append(" files deleted: ").append(filesDeleted);
548             logBuilder.append(" files moved: ").append(filesMoved);
549             Slog.i(TAG, logBuilder.toString());
550 
551             // Now re-index the new files.
552             indexFilesLocked();
553         }
554     }
555 
556     /**
557      * Get the latest stats that exist for this interval type.
558      */
getLatestUsageStats(int intervalType)559     public IntervalStats getLatestUsageStats(int intervalType) {
560         synchronized (mLock) {
561             if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
562                 throw new IllegalArgumentException("Bad interval type " + intervalType);
563             }
564 
565             final int fileCount = mSortedStatFiles[intervalType].size();
566             if (fileCount == 0) {
567                 return null;
568             }
569 
570             try {
571                 final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1);
572                 IntervalStats stats = new IntervalStats();
573                 readLocked(f, stats);
574                 return stats;
575             } catch (IOException e) {
576                 Slog.e(TAG, "Failed to read usage stats file", e);
577             }
578         }
579         return null;
580     }
581 
582     /**
583      * Figures out what to extract from the given IntervalStats object.
584      */
585     public interface StatCombiner<T> {
586 
587         /**
588          * Implementations should extract interesting from <code>stats</code> and add it
589          * to the <code>accumulatedResult</code> list.
590          *
591          * If the <code>stats</code> object is mutable, <code>mutable</code> will be true,
592          * which means you should make a copy of the data before adding it to the
593          * <code>accumulatedResult</code> list.
594          *
595          * @param stats             The {@link IntervalStats} object selected.
596          * @param mutable           Whether or not the data inside the stats object is mutable.
597          * @param accumulatedResult The list to which to add extracted data.
598          */
combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult)599         void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult);
600     }
601 
602     /**
603      * Find all {@link IntervalStats} for the given range and interval type.
604      */
queryUsageStats(int intervalType, long beginTime, long endTime, StatCombiner<T> combiner)605     public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime,
606             StatCombiner<T> combiner) {
607         synchronized (mLock) {
608             if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
609                 throw new IllegalArgumentException("Bad interval type " + intervalType);
610             }
611 
612             final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];
613 
614             if (endTime <= beginTime) {
615                 if (DEBUG) {
616                     Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")");
617                 }
618                 return null;
619             }
620 
621             int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
622             if (startIndex < 0) {
623                 // All the stats available have timestamps after beginTime, which means they all
624                 // match.
625                 startIndex = 0;
626             }
627 
628             int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
629             if (endIndex < 0) {
630                 // All the stats start after this range ends, so nothing matches.
631                 if (DEBUG) {
632                     Slog.d(TAG, "No results for this range. All stats start after.");
633                 }
634                 return null;
635             }
636 
637             if (intervalStats.keyAt(endIndex) == endTime) {
638                 // The endTime is exclusive, so if we matched exactly take the one before.
639                 endIndex--;
640                 if (endIndex < 0) {
641                     // All the stats start after this range ends, so nothing matches.
642                     if (DEBUG) {
643                         Slog.d(TAG, "No results for this range. All stats start after.");
644                     }
645                     return null;
646                 }
647             }
648 
649             final IntervalStats stats = new IntervalStats();
650             final ArrayList<T> results = new ArrayList<>();
651             for (int i = startIndex; i <= endIndex; i++) {
652                 final AtomicFile f = intervalStats.valueAt(i);
653 
654                 if (DEBUG) {
655                     Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
656                 }
657 
658                 try {
659                     readLocked(f, stats);
660                     if (beginTime < stats.endTime) {
661                         combiner.combine(stats, false, results);
662                     }
663                 } catch (IOException e) {
664                     Slog.e(TAG, "Failed to read usage stats file", e);
665                     // We continue so that we return results that are not
666                     // corrupt.
667                 }
668             }
669             return results;
670         }
671     }
672 
673     /**
674      * Find the interval that best matches this range.
675      *
676      * TODO(adamlesinski): Use endTimeStamp in best fit calculation.
677      */
findBestFitBucket(long beginTimeStamp, long endTimeStamp)678     public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) {
679         synchronized (mLock) {
680             int bestBucket = -1;
681             long smallestDiff = Long.MAX_VALUE;
682             for (int i = mSortedStatFiles.length - 1; i >= 0; i--) {
683                 final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp);
684                 int size = mSortedStatFiles[i].size();
685                 if (index >= 0 && index < size) {
686                     // We have some results here, check if they are better than our current match.
687                     long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp);
688                     if (diff < smallestDiff) {
689                         smallestDiff = diff;
690                         bestBucket = i;
691                     }
692                 }
693             }
694             return bestBucket;
695         }
696     }
697 
698     /**
699      * Remove any usage stat files that are too old.
700      */
prune(final long currentTimeMillis)701     public void prune(final long currentTimeMillis) {
702         synchronized (mLock) {
703             mCal.setTimeInMillis(currentTimeMillis);
704             mCal.addYears(-3);
705             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY],
706                     mCal.getTimeInMillis());
707 
708             mCal.setTimeInMillis(currentTimeMillis);
709             mCal.addMonths(-6);
710             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
711                     mCal.getTimeInMillis());
712 
713             mCal.setTimeInMillis(currentTimeMillis);
714             mCal.addWeeks(-4);
715             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
716                     mCal.getTimeInMillis());
717 
718             mCal.setTimeInMillis(currentTimeMillis);
719             mCal.addDays(-10);
720             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
721                     mCal.getTimeInMillis());
722 
723             mCal.setTimeInMillis(currentTimeMillis);
724             mCal.addDays(-SELECTION_LOG_RETENTION_LEN);
725             for (int i = 0; i < mIntervalDirs.length; ++i) {
726                 pruneChooserCountsOlderThan(mIntervalDirs[i], mCal.getTimeInMillis());
727             }
728 
729             // We must re-index our file list or we will be trying to read
730             // deleted files.
731             indexFilesLocked();
732         }
733     }
734 
pruneFilesOlderThan(File dir, long expiryTime)735     private static void pruneFilesOlderThan(File dir, long expiryTime) {
736         File[] files = dir.listFiles();
737         if (files != null) {
738             for (File f : files) {
739                 long beginTime;
740                 try {
741                     beginTime = parseBeginTime(f);
742                 } catch (IOException e) {
743                     beginTime = 0;
744                 }
745 
746                 if (beginTime < expiryTime) {
747                     new AtomicFile(f).delete();
748                 }
749             }
750         }
751     }
752 
pruneChooserCountsOlderThan(File dir, long expiryTime)753     private void pruneChooserCountsOlderThan(File dir, long expiryTime) {
754         File[] files = dir.listFiles();
755         if (files != null) {
756             for (File f : files) {
757                 long beginTime;
758                 try {
759                     beginTime = parseBeginTime(f);
760                 } catch (IOException e) {
761                     beginTime = 0;
762                 }
763 
764                 if (beginTime < expiryTime) {
765                     try {
766                         final AtomicFile af = new AtomicFile(f);
767                         final IntervalStats stats = new IntervalStats();
768                         readLocked(af, stats);
769                         final int pkgCount = stats.packageStats.size();
770                         for (int i = 0; i < pkgCount; i++) {
771                             UsageStats pkgStats = stats.packageStats.valueAt(i);
772                             if (pkgStats.mChooserCounts != null) {
773                                 pkgStats.mChooserCounts.clear();
774                             }
775                         }
776                         writeLocked(af, stats);
777                     } catch (Exception e) {
778                         Slog.e(TAG, "Failed to delete chooser counts from usage stats file", e);
779                     }
780                 }
781             }
782         }
783     }
784 
785 
parseBeginTime(AtomicFile file)786     private static long parseBeginTime(AtomicFile file) throws IOException {
787         return parseBeginTime(file.getBaseFile());
788     }
789 
parseBeginTime(File file)790     private static long parseBeginTime(File file) throws IOException {
791         String name = file.getName();
792 
793         // Parse out the digits from the the front of the file name
794         for (int i = 0; i < name.length(); i++) {
795             final char c = name.charAt(i);
796             if (c < '0' || c > '9') {
797                 // found first char that is not a digit.
798                 name = name.substring(0, i);
799                 break;
800             }
801         }
802 
803         try {
804             return Long.parseLong(name);
805         } catch (NumberFormatException e) {
806             throw new IOException(e);
807         }
808     }
809 
writeLocked(AtomicFile file, IntervalStats stats)810     private void writeLocked(AtomicFile file, IntervalStats stats) throws IOException {
811         writeLocked(file, stats, mCurrentVersion);
812     }
813 
writeLocked(AtomicFile file, IntervalStats stats, int version)814     private static void writeLocked(AtomicFile file, IntervalStats stats, int version)
815             throws IOException {
816         FileOutputStream fos = file.startWrite();
817         try {
818             writeLocked(fos, stats, version);
819             file.finishWrite(fos);
820             fos = null;
821         } finally {
822             // When fos is null (successful write), this will no-op
823             file.failWrite(fos);
824         }
825     }
826 
writeLocked(OutputStream out, IntervalStats stats)827     private void writeLocked(OutputStream out, IntervalStats stats) throws IOException {
828         writeLocked(out, stats, mCurrentVersion);
829     }
830 
writeLocked(OutputStream out, IntervalStats stats, int version)831     private static void writeLocked(OutputStream out, IntervalStats stats, int version)
832             throws IOException {
833         switch (version) {
834             case 1:
835             case 2:
836             case 3:
837                 UsageStatsXml.write(out, stats);
838                 break;
839             case 4:
840                 UsageStatsProto.write(out, stats);
841                 break;
842             default:
843                 throw new RuntimeException(
844                         "Unhandled UsageStatsDatabase version: " + Integer.toString(version)
845                                 + " on write.");
846         }
847     }
848 
readLocked(AtomicFile file, IntervalStats statsOut)849     private void readLocked(AtomicFile file, IntervalStats statsOut) throws IOException {
850         readLocked(file, statsOut, mCurrentVersion);
851     }
852 
readLocked(AtomicFile file, IntervalStats statsOut, int version)853     private static void readLocked(AtomicFile file, IntervalStats statsOut, int version)
854             throws IOException {
855         try {
856             FileInputStream in = file.openRead();
857             try {
858                 statsOut.beginTime = parseBeginTime(file);
859                 readLocked(in, statsOut, version);
860                 statsOut.lastTimeSaved = file.getLastModifiedTime();
861             } finally {
862                 try {
863                     in.close();
864                 } catch (IOException e) {
865                     // Empty
866                 }
867             }
868         } catch (FileNotFoundException e) {
869             Slog.e(TAG, "UsageStatsDatabase", e);
870             throw e;
871         }
872     }
873 
readLocked(InputStream in, IntervalStats statsOut)874     private void readLocked(InputStream in, IntervalStats statsOut) throws IOException {
875         readLocked(in, statsOut, mCurrentVersion);
876     }
877 
readLocked(InputStream in, IntervalStats statsOut, int version)878     private static void readLocked(InputStream in, IntervalStats statsOut, int version)
879             throws IOException {
880         switch (version) {
881             case 1:
882             case 2:
883             case 3:
884                 UsageStatsXml.read(in, statsOut);
885                 break;
886             case 4:
887                 UsageStatsProto.read(in, statsOut);
888                 break;
889             default:
890                 throw new RuntimeException(
891                         "Unhandled UsageStatsDatabase version: " + Integer.toString(version)
892                                 + " on read.");
893         }
894 
895     }
896 
897     /**
898      * Update the stats in the database. They may not be written to disk immediately.
899      */
putUsageStats(int intervalType, IntervalStats stats)900     public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
901         if (stats == null) return;
902         synchronized (mLock) {
903             if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
904                 throw new IllegalArgumentException("Bad interval type " + intervalType);
905             }
906 
907             AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
908             if (f == null) {
909                 f = new AtomicFile(new File(mIntervalDirs[intervalType],
910                         Long.toString(stats.beginTime)));
911                 mSortedStatFiles[intervalType].put(stats.beginTime, f);
912             }
913 
914             writeLocked(f, stats);
915             stats.lastTimeSaved = f.getLastModifiedTime();
916         }
917     }
918 
919 
920     /* Backup/Restore Code */
getBackupPayload(String key)921     byte[] getBackupPayload(String key) {
922         return getBackupPayload(key, BACKUP_VERSION);
923     }
924 
925     /**
926      * @hide
927      */
928     @VisibleForTesting
getBackupPayload(String key, int version)929     public byte[] getBackupPayload(String key, int version) {
930         synchronized (mLock) {
931             ByteArrayOutputStream baos = new ByteArrayOutputStream();
932             if (KEY_USAGE_STATS.equals(key)) {
933                 prune(System.currentTimeMillis());
934                 DataOutputStream out = new DataOutputStream(baos);
935                 try {
936                     out.writeInt(version);
937 
938                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size());
939 
940                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size();
941                             i++) {
942                         writeIntervalStatsToStream(out,
943                                 mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i),
944                                 version);
945                     }
946 
947                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size());
948                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size();
949                             i++) {
950                         writeIntervalStatsToStream(out,
951                                 mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i),
952                                 version);
953                     }
954 
955                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size());
956                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size();
957                             i++) {
958                         writeIntervalStatsToStream(out,
959                                 mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i),
960                                 version);
961                     }
962 
963                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size());
964                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size();
965                             i++) {
966                         writeIntervalStatsToStream(out,
967                                 mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i),
968                                 version);
969                     }
970                     if (DEBUG) Slog.i(TAG, "Written " + baos.size() + " bytes of data");
971                 } catch (IOException ioe) {
972                     Slog.d(TAG, "Failed to write data to output stream", ioe);
973                     baos.reset();
974                 }
975             }
976             return baos.toByteArray();
977         }
978 
979     }
980 
981     /**
982      * @hide
983      */
984     @VisibleForTesting
applyRestoredPayload(String key, byte[] payload)985     public void applyRestoredPayload(String key, byte[] payload) {
986         synchronized (mLock) {
987             if (KEY_USAGE_STATS.equals(key)) {
988                 // Read stats files for the current device configs
989                 IntervalStats dailyConfigSource =
990                         getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY);
991                 IntervalStats weeklyConfigSource =
992                         getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY);
993                 IntervalStats monthlyConfigSource =
994                         getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY);
995                 IntervalStats yearlyConfigSource =
996                         getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);
997 
998                 try {
999                     DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
1000                     int backupDataVersion = in.readInt();
1001 
1002                     // Can't handle this backup set
1003                     if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return;
1004 
1005                     // Delete all stats files
1006                     // Do this after reading version and before actually restoring
1007                     for (int i = 0; i < mIntervalDirs.length; i++) {
1008                         deleteDirectoryContents(mIntervalDirs[i]);
1009                     }
1010 
1011                     int fileCount = in.readInt();
1012                     for (int i = 0; i < fileCount; i++) {
1013                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
1014                                 backupDataVersion);
1015                         stats = mergeStats(stats, dailyConfigSource);
1016                         putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats);
1017                     }
1018 
1019                     fileCount = in.readInt();
1020                     for (int i = 0; i < fileCount; i++) {
1021                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
1022                                 backupDataVersion);
1023                         stats = mergeStats(stats, weeklyConfigSource);
1024                         putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats);
1025                     }
1026 
1027                     fileCount = in.readInt();
1028                     for (int i = 0; i < fileCount; i++) {
1029                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
1030                                 backupDataVersion);
1031                         stats = mergeStats(stats, monthlyConfigSource);
1032                         putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats);
1033                     }
1034 
1035                     fileCount = in.readInt();
1036                     for (int i = 0; i < fileCount; i++) {
1037                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
1038                                 backupDataVersion);
1039                         stats = mergeStats(stats, yearlyConfigSource);
1040                         putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats);
1041                     }
1042                     if (DEBUG) Slog.i(TAG, "Completed Restoring UsageStats");
1043                 } catch (IOException ioe) {
1044                     Slog.d(TAG, "Failed to read data from input stream", ioe);
1045                 } finally {
1046                     indexFilesLocked();
1047                 }
1048             }
1049         }
1050     }
1051 
1052     /**
1053      * Get the Configuration Statistics from the current device statistics and merge them
1054      * with the backed up usage statistics.
1055      */
mergeStats(IntervalStats beingRestored, IntervalStats onDevice)1056     private IntervalStats mergeStats(IntervalStats beingRestored, IntervalStats onDevice) {
1057         if (onDevice == null) return beingRestored;
1058         if (beingRestored == null) return null;
1059         beingRestored.activeConfiguration = onDevice.activeConfiguration;
1060         beingRestored.configurations.putAll(onDevice.configurations);
1061         beingRestored.events.clear();
1062         beingRestored.events.merge(onDevice.events);
1063         return beingRestored;
1064     }
1065 
writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version)1066     private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version)
1067             throws IOException {
1068         IntervalStats stats = new IntervalStats();
1069         try {
1070             readLocked(statsFile, stats);
1071         } catch (IOException e) {
1072             Slog.e(TAG, "Failed to read usage stats file", e);
1073             out.writeInt(0);
1074             return;
1075         }
1076         sanitizeIntervalStatsForBackup(stats);
1077         byte[] data = serializeIntervalStats(stats, version);
1078         out.writeInt(data.length);
1079         out.write(data);
1080     }
1081 
getIntervalStatsBytes(DataInputStream in)1082     private static byte[] getIntervalStatsBytes(DataInputStream in) throws IOException {
1083         int length = in.readInt();
1084         byte[] buffer = new byte[length];
1085         in.read(buffer, 0, length);
1086         return buffer;
1087     }
1088 
sanitizeIntervalStatsForBackup(IntervalStats stats)1089     private static void sanitizeIntervalStatsForBackup(IntervalStats stats) {
1090         if (stats == null) return;
1091         stats.activeConfiguration = null;
1092         stats.configurations.clear();
1093         stats.events.clear();
1094     }
1095 
serializeIntervalStats(IntervalStats stats, int version)1096     private byte[] serializeIntervalStats(IntervalStats stats, int version) {
1097         ByteArrayOutputStream baos = new ByteArrayOutputStream();
1098         DataOutputStream out = new DataOutputStream(baos);
1099         try {
1100             out.writeLong(stats.beginTime);
1101             writeLocked(out, stats, version);
1102         } catch (Exception ioe) {
1103             Slog.d(TAG, "Serializing IntervalStats Failed", ioe);
1104             baos.reset();
1105         }
1106         return baos.toByteArray();
1107     }
1108 
deserializeIntervalStats(byte[] data, int version)1109     private IntervalStats deserializeIntervalStats(byte[] data, int version) {
1110         ByteArrayInputStream bais = new ByteArrayInputStream(data);
1111         DataInputStream in = new DataInputStream(bais);
1112         IntervalStats stats = new IntervalStats();
1113         try {
1114             stats.beginTime = in.readLong();
1115             readLocked(in, stats, version);
1116         } catch (IOException ioe) {
1117             Slog.d(TAG, "DeSerializing IntervalStats Failed", ioe);
1118             stats = null;
1119         }
1120         return stats;
1121     }
1122 
deleteDirectoryContents(File directory)1123     private static void deleteDirectoryContents(File directory) {
1124         File[] files = directory.listFiles();
1125         for (File file : files) {
1126             deleteDirectory(file);
1127         }
1128     }
1129 
deleteDirectory(File directory)1130     private static void deleteDirectory(File directory) {
1131         File[] files = directory.listFiles();
1132         if (files != null) {
1133             for (File file : files) {
1134                 if (!file.isDirectory()) {
1135                     file.delete();
1136                 } else {
1137                     deleteDirectory(file);
1138                 }
1139             }
1140         }
1141         directory.delete();
1142     }
1143 
1144     /**
1145      * print total number and list of stats files for each interval type.
1146      * @param pw
1147      */
dump(IndentingPrintWriter pw, boolean compact)1148     public void dump(IndentingPrintWriter pw, boolean compact) {
1149         synchronized (mLock) {
1150             pw.println("UsageStatsDatabase:");
1151             pw.increaseIndent();
1152             for (int i = 0; i < mSortedStatFiles.length; i++) {
1153                 final TimeSparseArray<AtomicFile> files = mSortedStatFiles[i];
1154                 final int size = files.size();
1155                 pw.print(UserUsageStatsService.intervalToString(i));
1156                 pw.print(" stats files: ");
1157                 pw.print(size);
1158                 pw.println(", sorted list of files:");
1159                 pw.increaseIndent();
1160                 for (int f = 0; f < size; f++) {
1161                     final long fileName = files.keyAt(f);
1162                     if (compact) {
1163                         pw.print(UserUsageStatsService.formatDateTime(fileName, false));
1164                     } else {
1165                         pw.printPair(Long.toString(fileName),
1166                                 UserUsageStatsService.formatDateTime(fileName, true));
1167                     }
1168                     pw.println();
1169                 }
1170                 pw.decreaseIndent();
1171             }
1172             pw.decreaseIndent();
1173         }
1174     }
1175 
readIntervalStatsForFile(int interval, long fileName)1176     IntervalStats readIntervalStatsForFile(int interval, long fileName) {
1177         synchronized (mLock) {
1178             final IntervalStats stats = new IntervalStats();
1179             try {
1180                 readLocked(mSortedStatFiles[interval].get(fileName, null), stats);
1181                 return stats;
1182             } catch (Exception e) {
1183                 return null;
1184             }
1185         }
1186     }
1187 }
1188