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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.usage.UsageEvents;
22 import android.app.usage.UsageStats;
23 import android.app.usage.UsageStatsManager;
24 import android.os.Build;
25 import android.os.SystemClock;
26 import android.os.SystemProperties;
27 import android.util.ArrayMap;
28 import android.util.ArraySet;
29 import android.util.AtomicFile;
30 import android.util.Slog;
31 import android.util.SparseArray;
32 import android.util.TimeSparseArray;
33 import android.util.TimeUtils;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.ArrayUtils;
37 import com.android.internal.util.IndentingPrintWriter;
38 
39 import libcore.io.IoUtils;
40 
41 import java.io.BufferedReader;
42 import java.io.BufferedWriter;
43 import java.io.ByteArrayInputStream;
44 import java.io.ByteArrayOutputStream;
45 import java.io.DataInputStream;
46 import java.io.DataOutputStream;
47 import java.io.File;
48 import java.io.FileInputStream;
49 import java.io.FileNotFoundException;
50 import java.io.FileOutputStream;
51 import java.io.FileReader;
52 import java.io.FileWriter;
53 import java.io.FilenameFilter;
54 import java.io.IOException;
55 import java.io.InputStream;
56 import java.io.OutputStream;
57 import java.nio.file.Files;
58 import java.nio.file.StandardCopyOption;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.Collections;
62 import java.util.HashMap;
63 import java.util.List;
64 import java.util.Set;
65 import java.util.concurrent.TimeUnit;
66 
67 /**
68  * Provides an interface to query for UsageStat data from a Protocol Buffer database.
69  *
70  * Prior to version 4, UsageStatsDatabase used XML to store Usage Stats data to disk.
71  * When the UsageStatsDatabase version is upgraded, the files on disk are migrated to the new
72  * version on init. The steps of migration are as follows:
73  * 1) Check if version upgrade breadcrumb exists on disk, if so skip to step 4.
74  * 2) Move current files to a timestamped backup directory.
75  * 3) Write a temporary breadcrumb file with some info about the backup directory.
76  * 4) Deserialize the backup files in the timestamped backup folder referenced by the breadcrumb.
77  * 5) Reserialize the data read from the file with the new version format and replace the old files
78  * 6) Repeat Step 3 and 4 for each file in the backup folder.
79  * 7) Update the version file with the new version and build fingerprint.
80  * 8) Delete the time stamped backup folder (unless flagged to be kept).
81  * 9) Delete the breadcrumb file.
82  *
83  * Performing the upgrade steps in this order, protects against unexpected shutdowns mid upgrade
84  *
85  * The backup directory will contain directories with timestamp names. If the upgrade breadcrumb
86  * exists on disk, it will contain a timestamp which will match one of the backup directories. The
87  * breadcrumb will also contain a version number which will denote how the files in the backup
88  * directory should be deserialized.
89  */
90 public class UsageStatsDatabase {
91     private static final int DEFAULT_CURRENT_VERSION = 5;
92     /**
93      * Current version of the backup schema
94      *
95      * @hide
96      */
97     @VisibleForTesting
98     public static final int BACKUP_VERSION = 4;
99 
100     @VisibleForTesting
101     static final int[] MAX_FILES_PER_INTERVAL_TYPE = new int[]{100, 50, 12, 10};
102 
103     // Key under which the payload blob is stored
104     // same as UsageStatsBackupHelper.KEY_USAGE_STATS
105     static final String KEY_USAGE_STATS = "usage_stats";
106 
107     // Persist versioned backup files.
108     // Should be false, except when testing new versions
109     static final boolean KEEP_BACKUP_DIR = false;
110 
111     private static final String TAG = "UsageStatsDatabase";
112     private static final boolean DEBUG = UsageStatsService.DEBUG;
113     private static final String BAK_SUFFIX = ".bak";
114     private static final String CHECKED_IN_SUFFIX = UsageStatsXml.CHECKED_IN_SUFFIX;
115     private static final String RETENTION_LEN_KEY = "ro.usagestats.chooser.retention";
116     private static final int SELECTION_LOG_RETENTION_LEN =
117             SystemProperties.getInt(RETENTION_LEN_KEY, 14);
118 
119     private final Object mLock = new Object();
120     private final File[] mIntervalDirs;
121     @VisibleForTesting
122     final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
123     private final UnixCalendar mCal;
124     private final File mVersionFile;
125     private final File mBackupsDir;
126     // If this file exists on disk, UsageStatsDatabase is in the middle of migrating files to a new
127     // version. If this file exists on boot, the upgrade was interrupted and needs to be picked up
128     // where it left off.
129     private final File mUpdateBreadcrumb;
130     // Current version of the database files schema
131     private int mCurrentVersion;
132     private boolean mFirstUpdate;
133     private boolean mNewUpdate;
134     private boolean mUpgradePerformed;
135 
136     // The obfuscated packages to tokens mappings file
137     private final File mPackageMappingsFile;
138     // Holds all of the data related to the obfuscated packages and their token mappings.
139     final PackagesTokenData mPackagesTokenData = new PackagesTokenData();
140 
141     /**
142      * UsageStatsDatabase constructor that allows setting the version number.
143      * This should only be used for testing.
144      *
145      * @hide
146      */
147     @VisibleForTesting
UsageStatsDatabase(File dir, int version)148     public UsageStatsDatabase(File dir, int version) {
149         mIntervalDirs = new File[]{
150             new File(dir, "daily"),
151             new File(dir, "weekly"),
152             new File(dir, "monthly"),
153             new File(dir, "yearly"),
154         };
155         mCurrentVersion = version;
156         mVersionFile = new File(dir, "version");
157         mBackupsDir = new File(dir, "backups");
158         mUpdateBreadcrumb = new File(dir, "breadcrumb");
159         mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
160         mPackageMappingsFile = new File(dir, "mappings");
161         mCal = new UnixCalendar(0);
162     }
163 
UsageStatsDatabase(File dir)164     public UsageStatsDatabase(File dir) {
165         this(dir, DEFAULT_CURRENT_VERSION);
166     }
167 
168     /**
169      * Initialize any directories required and index what stats are available.
170      */
init(long currentTimeMillis)171     public void init(long currentTimeMillis) {
172         synchronized (mLock) {
173             for (File f : mIntervalDirs) {
174                 f.mkdirs();
175                 if (!f.exists()) {
176                     throw new IllegalStateException("Failed to create directory "
177                             + f.getAbsolutePath());
178                 }
179             }
180 
181             checkVersionAndBuildLocked();
182             indexFilesLocked();
183 
184             // Delete files that are in the future.
185             for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) {
186                 final int startIndex = files.closestIndexOnOrAfter(currentTimeMillis);
187                 if (startIndex < 0) {
188                     continue;
189                 }
190 
191                 final int fileCount = files.size();
192                 for (int i = startIndex; i < fileCount; i++) {
193                     files.valueAt(i).delete();
194                 }
195 
196                 // Remove in a separate loop because any accesses (valueAt)
197                 // will cause a gc in the SparseArray and mess up the order.
198                 for (int i = startIndex; i < fileCount; i++) {
199                     files.removeAt(i);
200                 }
201             }
202         }
203     }
204 
205     public interface CheckinAction {
checkin(IntervalStats stats)206         boolean checkin(IntervalStats stats);
207     }
208 
209     /**
210      * Calls {@link CheckinAction#checkin(IntervalStats)} on the given {@link CheckinAction}
211      * for all {@link IntervalStats} that haven't been checked-in.
212      * If any of the calls to {@link CheckinAction#checkin(IntervalStats)} returns false or throws
213      * an exception, the check-in will be aborted.
214      *
215      * @param checkinAction The callback to run when checking-in {@link IntervalStats}.
216      * @return true if the check-in succeeded.
217      */
checkinDailyFiles(CheckinAction checkinAction)218     public boolean checkinDailyFiles(CheckinAction checkinAction) {
219         synchronized (mLock) {
220             final TimeSparseArray<AtomicFile> files =
221                     mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY];
222             final int fileCount = files.size();
223 
224             // We may have holes in the checkin (if there was an error)
225             // so find the last checked-in file and go from there.
226             int lastCheckin = -1;
227             for (int i = 0; i < fileCount - 1; i++) {
228                 if (files.valueAt(i).getBaseFile().getPath().endsWith(CHECKED_IN_SUFFIX)) {
229                     lastCheckin = i;
230                 }
231             }
232 
233             final int start = lastCheckin + 1;
234             if (start == fileCount - 1) {
235                 return true;
236             }
237 
238             try {
239                 for (int i = start; i < fileCount - 1; i++) {
240                     final IntervalStats stats = new IntervalStats();
241                     readLocked(files.valueAt(i), stats);
242                     if (!checkinAction.checkin(stats)) {
243                         return false;
244                     }
245                 }
246             } catch (Exception e) {
247                 Slog.e(TAG, "Failed to check-in", e);
248                 return false;
249             }
250 
251             // We have successfully checked-in the stats, so rename the files so that they
252             // are marked as checked-in.
253             for (int i = start; i < fileCount - 1; i++) {
254                 final AtomicFile file = files.valueAt(i);
255                 final File checkedInFile = new File(
256                         file.getBaseFile().getPath() + CHECKED_IN_SUFFIX);
257                 if (!file.getBaseFile().renameTo(checkedInFile)) {
258                     // We must return success, as we've already marked some files as checked-in.
259                     // It's better to repeat ourselves than to lose data.
260                     Slog.e(TAG, "Failed to mark file " + file.getBaseFile().getPath()
261                             + " as checked-in");
262                     return true;
263                 }
264 
265                 // AtomicFile needs to set a new backup path with the same -c extension, so
266                 // we replace the old AtomicFile with the updated one.
267                 files.setValueAt(i, new AtomicFile(checkedInFile));
268             }
269         }
270         return true;
271     }
272 
273     /** @hide */
274     @VisibleForTesting
forceIndexFiles()275     void forceIndexFiles() {
276         synchronized (mLock) {
277             indexFilesLocked();
278         }
279     }
280 
indexFilesLocked()281     private void indexFilesLocked() {
282         final FilenameFilter backupFileFilter = new FilenameFilter() {
283             @Override
284             public boolean accept(File dir, String name) {
285                 return !name.endsWith(BAK_SUFFIX);
286             }
287         };
288         // Index the available usage stat files on disk.
289         for (int i = 0; i < mSortedStatFiles.length; i++) {
290             if (mSortedStatFiles[i] == null) {
291                 mSortedStatFiles[i] = new TimeSparseArray<>();
292             } else {
293                 mSortedStatFiles[i].clear();
294             }
295             File[] files = mIntervalDirs[i].listFiles(backupFileFilter);
296             if (files != null) {
297                 if (DEBUG) {
298                     Slog.d(TAG, "Found " + files.length + " stat files for interval " + i);
299                 }
300                 final int len = files.length;
301                 for (int j = 0; j < len; j++) {
302                     final File f = files[j];
303                     final AtomicFile af = new AtomicFile(f);
304                     try {
305                         mSortedStatFiles[i].put(parseBeginTime(af), af);
306                     } catch (IOException e) {
307                         Slog.e(TAG, "failed to index file: " + f, e);
308                     }
309                 }
310 
311                 // only keep the max allowed number of files for each interval type.
312                 final int toDelete = mSortedStatFiles[i].size() - MAX_FILES_PER_INTERVAL_TYPE[i];
313                 if (toDelete > 0) {
314                     for (int j = 0; j < toDelete; j++) {
315                         mSortedStatFiles[i].valueAt(0).delete();
316                         mSortedStatFiles[i].removeAt(0);
317                     }
318                     Slog.d(TAG, "Deleted " + toDelete + " stat files for interval " + i);
319                 }
320             }
321         }
322     }
323 
324     /**
325      * Is this the first update to the system from L to M?
326      */
isFirstUpdate()327     boolean isFirstUpdate() {
328         return mFirstUpdate;
329     }
330 
331     /**
332      * Is this a system update since we started tracking build fingerprint in the version file?
333      */
isNewUpdate()334     boolean isNewUpdate() {
335         return mNewUpdate;
336     }
337 
338     /**
339      * Was an upgrade performed when this database was initialized?
340      */
wasUpgradePerformed()341     boolean wasUpgradePerformed() {
342         return mUpgradePerformed;
343     }
344 
checkVersionAndBuildLocked()345     private void checkVersionAndBuildLocked() {
346         int version;
347         String buildFingerprint;
348         String currentFingerprint = getBuildFingerprint();
349         mFirstUpdate = true;
350         mNewUpdate = true;
351         try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
352             version = Integer.parseInt(reader.readLine());
353             buildFingerprint = reader.readLine();
354             if (buildFingerprint != null) {
355                 mFirstUpdate = false;
356             }
357             if (currentFingerprint.equals(buildFingerprint)) {
358                 mNewUpdate = false;
359             }
360         } catch (NumberFormatException | IOException e) {
361             version = 0;
362         }
363 
364         if (version != mCurrentVersion) {
365             Slog.i(TAG, "Upgrading from version " + version + " to " + mCurrentVersion);
366             if (!mUpdateBreadcrumb.exists()) {
367                 try {
368                     doUpgradeLocked(version);
369                 } catch (Exception e) {
370                     Slog.e(TAG,
371                             "Failed to upgrade from version " + version + " to " + mCurrentVersion,
372                             e);
373                     // Fallback to previous version.
374                     mCurrentVersion = version;
375                     return;
376                 }
377             } else {
378                 Slog.i(TAG, "Version upgrade breadcrumb found on disk! Continuing version upgrade");
379             }
380         }
381 
382         if (mUpdateBreadcrumb.exists()) {
383             int previousVersion;
384             long token;
385             try (BufferedReader reader = new BufferedReader(
386                     new FileReader(mUpdateBreadcrumb))) {
387                 token = Long.parseLong(reader.readLine());
388                 previousVersion = Integer.parseInt(reader.readLine());
389             } catch (NumberFormatException | IOException e) {
390                 Slog.e(TAG, "Failed read version upgrade breadcrumb");
391                 throw new RuntimeException(e);
392             }
393             if (mCurrentVersion >= 4) {
394                 continueUpgradeLocked(previousVersion, token);
395             } else {
396                 Slog.wtf(TAG, "Attempting to upgrade to an unsupported version: "
397                         + mCurrentVersion);
398             }
399         }
400 
401         if (version != mCurrentVersion || mNewUpdate) {
402             try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
403                 writer.write(Integer.toString(mCurrentVersion));
404                 writer.write("\n");
405                 writer.write(currentFingerprint);
406                 writer.write("\n");
407                 writer.flush();
408             } catch (IOException e) {
409                 Slog.e(TAG, "Failed to write new version");
410                 throw new RuntimeException(e);
411             }
412         }
413 
414         if (mUpdateBreadcrumb.exists()) {
415             // Files should be up to date with current version. Clear the version update breadcrumb
416             mUpdateBreadcrumb.delete();
417             // update mUpgradePerformed after breadcrumb is deleted to indicate a successful upgrade
418             mUpgradePerformed = true;
419         }
420 
421         if (mBackupsDir.exists() && !KEEP_BACKUP_DIR) {
422             mUpgradePerformed = true; // updated here to ensure that data is cleaned up
423             deleteDirectory(mBackupsDir);
424         }
425     }
426 
getBuildFingerprint()427     private String getBuildFingerprint() {
428         return Build.VERSION.RELEASE + ";"
429                 + Build.VERSION.CODENAME + ";"
430                 + Build.VERSION.INCREMENTAL;
431     }
432 
doUpgradeLocked(int thisVersion)433     private void doUpgradeLocked(int thisVersion) {
434         if (thisVersion < 2) {
435             // Delete all files if we are version 0. This is a pre-release version,
436             // so this is fine.
437             Slog.i(TAG, "Deleting all usage stats files");
438             for (int i = 0; i < mIntervalDirs.length; i++) {
439                 File[] files = mIntervalDirs[i].listFiles();
440                 if (files != null) {
441                     for (File f : files) {
442                         f.delete();
443                     }
444                 }
445             }
446         } else {
447             // Create a dir in backups based on current timestamp
448             final long token = System.currentTimeMillis();
449             final File backupDir = new File(mBackupsDir, Long.toString(token));
450             backupDir.mkdirs();
451             if (!backupDir.exists()) {
452                 throw new IllegalStateException(
453                         "Failed to create backup directory " + backupDir.getAbsolutePath());
454             }
455             try {
456                 Files.copy(mVersionFile.toPath(),
457                         new File(backupDir, mVersionFile.getName()).toPath(),
458                         StandardCopyOption.REPLACE_EXISTING);
459             } catch (IOException e) {
460                 Slog.e(TAG, "Failed to back up version file : " + mVersionFile.toString());
461                 throw new RuntimeException(e);
462             }
463 
464             for (int i = 0; i < mIntervalDirs.length; i++) {
465                 final File backupIntervalDir = new File(backupDir, mIntervalDirs[i].getName());
466                 backupIntervalDir.mkdir();
467 
468                 if (!backupIntervalDir.exists()) {
469                     throw new IllegalStateException(
470                             "Failed to create interval backup directory "
471                                     + backupIntervalDir.getAbsolutePath());
472                 }
473                 File[] files = mIntervalDirs[i].listFiles();
474                 if (files != null) {
475                     for (int j = 0; j < files.length; j++) {
476                         final File backupFile = new File(backupIntervalDir, files[j].getName());
477                         if (DEBUG) {
478                             Slog.d(TAG, "Creating versioned (" + Integer.toString(thisVersion)
479                                     + ") backup of " + files[j].toString()
480                                     + " stat files for interval "
481                                     + i + " to " + backupFile.toString());
482                         }
483 
484                         try {
485                             // Backup file should not already exist, but make sure it doesn't
486                             Files.move(files[j].toPath(), backupFile.toPath(),
487                                     StandardCopyOption.REPLACE_EXISTING);
488                         } catch (IOException e) {
489                             Slog.e(TAG, "Failed to back up file : " + files[j].toString());
490                             throw new RuntimeException(e);
491                         }
492                     }
493                 }
494             }
495 
496             // Leave a breadcrumb behind noting that all the usage stats have been moved to a backup
497             BufferedWriter writer = null;
498             try {
499                 writer = new BufferedWriter(new FileWriter(mUpdateBreadcrumb));
500                 writer.write(Long.toString(token));
501                 writer.write("\n");
502                 writer.write(Integer.toString(thisVersion));
503                 writer.write("\n");
504                 writer.flush();
505             } catch (IOException e) {
506                 Slog.e(TAG, "Failed to write new version upgrade breadcrumb");
507                 throw new RuntimeException(e);
508             } finally {
509                 IoUtils.closeQuietly(writer);
510             }
511         }
512     }
513 
continueUpgradeLocked(int version, long token)514     private void continueUpgradeLocked(int version, long token) {
515         if (version <= 3) {
516             Slog.w(TAG, "Reading UsageStats as XML; current database version: " + mCurrentVersion);
517         }
518         final File backupDir = new File(mBackupsDir, Long.toString(token));
519 
520         // Upgrade step logic for the entire usage stats directory, not individual interval dirs.
521         if (version >= 5) {
522             readMappingsLocked();
523         }
524 
525         // Read each file in the backup according to the version and write to the interval
526         // directories in the current versions format
527         for (int i = 0; i < mIntervalDirs.length; i++) {
528             final File backedUpInterval = new File(backupDir, mIntervalDirs[i].getName());
529             File[] files = backedUpInterval.listFiles();
530             if (files != null) {
531                 for (int j = 0; j < files.length; j++) {
532                     if (DEBUG) {
533                         Slog.d(TAG,
534                                 "Upgrading " + files[j].toString() + " to version ("
535                                         + Integer.toString(
536                                         mCurrentVersion) + ") for interval " + i);
537                     }
538                     try {
539                         IntervalStats stats = new IntervalStats();
540                         readLocked(new AtomicFile(files[j]), stats, version, mPackagesTokenData);
541                         // Upgrade to version 5+.
542                         // Future version upgrades should add additional logic here to upgrade.
543                         if (mCurrentVersion >= 5) {
544                             // Create the initial obfuscated packages map.
545                             stats.obfuscateData(mPackagesTokenData);
546                         }
547                         writeLocked(new AtomicFile(new File(mIntervalDirs[i],
548                                 Long.toString(stats.beginTime))), stats, mCurrentVersion,
549                                 mPackagesTokenData);
550                     } catch (Exception e) {
551                         // This method is called on boot, log the exception and move on
552                         Slog.e(TAG, "Failed to upgrade backup file : " + files[j].toString());
553                     }
554                 }
555             }
556         }
557 
558         // Upgrade step logic for the entire usage stats directory, not individual interval dirs.
559         if (mCurrentVersion >= 5) {
560             try {
561                 writeMappingsLocked();
562             } catch (IOException e) {
563                 Slog.e(TAG, "Failed to write the tokens mappings file.");
564             }
565         }
566     }
567 
568     /**
569      * Returns the token mapped to the package removed or {@code PackagesTokenData.UNASSIGNED_TOKEN}
570      * if not mapped.
571      */
onPackageRemoved(String packageName, long timeRemoved)572     int onPackageRemoved(String packageName, long timeRemoved) {
573         synchronized (mLock) {
574             final int tokenRemoved = mPackagesTokenData.removePackage(packageName, timeRemoved);
575             try {
576                 writeMappingsLocked();
577             } catch (Exception e) {
578                 Slog.w(TAG, "Unable to update package mappings on disk after removing token "
579                         + tokenRemoved);
580             }
581             return tokenRemoved;
582         }
583     }
584 
585     /**
586      * Reads all the usage stats data on disk and rewrites it with any data related to uninstalled
587      * packages omitted. Returns {@code true} on success, {@code false} otherwise.
588      */
pruneUninstalledPackagesData()589     boolean pruneUninstalledPackagesData() {
590         synchronized (mLock) {
591             for (int i = 0; i < mIntervalDirs.length; i++) {
592                 final File[] files = mIntervalDirs[i].listFiles();
593                 if (files == null) {
594                     continue;
595                 }
596                 for (int j = 0; j < files.length; j++) {
597                     try {
598                         final IntervalStats stats = new IntervalStats();
599                         final AtomicFile atomicFile = new AtomicFile(files[j]);
600                         if (!readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData)) {
601                             continue; // no data was omitted when read so no need to rewrite
602                         }
603                         // Any data related to packages that have been removed would have failed
604                         // the deobfuscation step on read so the IntervalStats object here only
605                         // contains data for packages that are currently installed - all we need
606                         // to do here is write the data back to disk.
607                         writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData);
608                     } catch (Exception e) {
609                         Slog.e(TAG, "Failed to prune data from: " + files[j].toString());
610                         return false;
611                     }
612                 }
613             }
614 
615             try {
616                 writeMappingsLocked();
617             } catch (IOException e) {
618                 Slog.e(TAG, "Failed to write package mappings after pruning data.");
619                 return false;
620             }
621             return true;
622         }
623     }
624 
625     /**
626      * Iterates through all the files on disk and prunes any data that belongs to packages that have
627      * been uninstalled (packages that are not in the given list).
628      * Note: this should only be called once, when there has been a database upgrade.
629      *
630      * @param installedPackages map of installed packages (package_name:package_install_time)
631      */
prunePackagesDataOnUpgrade(HashMap<String, Long> installedPackages)632     void prunePackagesDataOnUpgrade(HashMap<String, Long> installedPackages) {
633         if (ArrayUtils.isEmpty(installedPackages)) {
634             return;
635         }
636         synchronized (mLock) {
637             for (int i = 0; i < mIntervalDirs.length; i++) {
638                 final File[] files = mIntervalDirs[i].listFiles();
639                 if (files == null) {
640                     continue;
641                 }
642                 for (int j = 0; j < files.length; j++) {
643                     try {
644                         final IntervalStats stats = new IntervalStats();
645                         final AtomicFile atomicFile = new AtomicFile(files[j]);
646                         readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData);
647                         if (!pruneStats(installedPackages, stats)) {
648                             continue; // no stats were pruned so no need to rewrite
649                         }
650                         writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData);
651                     } catch (Exception e) {
652                         Slog.e(TAG, "Failed to prune data from: " + files[j].toString());
653                     }
654                 }
655             }
656         }
657     }
658 
pruneStats(HashMap<String, Long> installedPackages, IntervalStats stats)659     private boolean pruneStats(HashMap<String, Long> installedPackages, IntervalStats stats) {
660         boolean dataPruned = false;
661 
662         // prune old package usage stats
663         for (int i = stats.packageStats.size() - 1; i >= 0; i--) {
664             final UsageStats usageStats = stats.packageStats.valueAt(i);
665             final Long timeInstalled = installedPackages.get(usageStats.mPackageName);
666             if (timeInstalled == null || timeInstalled > usageStats.mEndTimeStamp) {
667                 stats.packageStats.removeAt(i);
668                 dataPruned = true;
669             }
670         }
671         if (dataPruned) {
672             // ensure old stats don't linger around during the obfuscation step on write
673             stats.packageStatsObfuscated.clear();
674         }
675 
676         // prune old events
677         for (int i = stats.events.size() - 1; i >= 0; i--) {
678             final UsageEvents.Event event = stats.events.get(i);
679             final Long timeInstalled = installedPackages.get(event.mPackage);
680             if (timeInstalled == null || timeInstalled > event.mTimeStamp) {
681                 stats.events.remove(i);
682                 dataPruned = true;
683             }
684         }
685 
686         return dataPruned;
687     }
688 
onTimeChanged(long timeDiffMillis)689     public void onTimeChanged(long timeDiffMillis) {
690         synchronized (mLock) {
691             StringBuilder logBuilder = new StringBuilder();
692             logBuilder.append("Time changed by ");
693             TimeUtils.formatDuration(timeDiffMillis, logBuilder);
694             logBuilder.append(".");
695 
696             int filesDeleted = 0;
697             int filesMoved = 0;
698 
699             for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) {
700                 final int fileCount = files.size();
701                 for (int i = 0; i < fileCount; i++) {
702                     final AtomicFile file = files.valueAt(i);
703                     final long newTime = files.keyAt(i) + timeDiffMillis;
704                     if (newTime < 0) {
705                         filesDeleted++;
706                         file.delete();
707                     } else {
708                         try {
709                             file.openRead().close();
710                         } catch (IOException e) {
711                             // Ignore, this is just to make sure there are no backups.
712                         }
713 
714                         String newName = Long.toString(newTime);
715                         if (file.getBaseFile().getName().endsWith(CHECKED_IN_SUFFIX)) {
716                             newName = newName + CHECKED_IN_SUFFIX;
717                         }
718 
719                         final File newFile = new File(file.getBaseFile().getParentFile(), newName);
720                         filesMoved++;
721                         file.getBaseFile().renameTo(newFile);
722                     }
723                 }
724                 files.clear();
725             }
726 
727             logBuilder.append(" files deleted: ").append(filesDeleted);
728             logBuilder.append(" files moved: ").append(filesMoved);
729             Slog.i(TAG, logBuilder.toString());
730 
731             // Now re-index the new files.
732             indexFilesLocked();
733         }
734     }
735 
736     /**
737      * Get the latest stats that exist for this interval type.
738      */
getLatestUsageStats(int intervalType)739     public IntervalStats getLatestUsageStats(int intervalType) {
740         synchronized (mLock) {
741             if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
742                 throw new IllegalArgumentException("Bad interval type " + intervalType);
743             }
744 
745             final int fileCount = mSortedStatFiles[intervalType].size();
746             if (fileCount == 0) {
747                 return null;
748             }
749 
750             try {
751                 final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1);
752                 IntervalStats stats = new IntervalStats();
753                 readLocked(f, stats);
754                 return stats;
755             } catch (Exception e) {
756                 Slog.e(TAG, "Failed to read usage stats file", e);
757             }
758         }
759         return null;
760     }
761 
762     /**
763      * Filter out those stats from the given stats that belong to removed packages. Filtering out
764      * all of the stats at once has an amortized cost for future calls.
765      */
filterStats(IntervalStats stats)766     void filterStats(IntervalStats stats) {
767         if (mPackagesTokenData.removedPackagesMap.isEmpty()) {
768             return;
769         }
770         final ArrayMap<String, Long> removedPackagesMap = mPackagesTokenData.removedPackagesMap;
771 
772         // filter out package usage stats
773         final int removedPackagesSize = removedPackagesMap.size();
774         for (int i = 0; i < removedPackagesSize; i++) {
775             final String removedPackage = removedPackagesMap.keyAt(i);
776             final UsageStats usageStats = stats.packageStats.get(removedPackage);
777             if (usageStats != null && usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) {
778                 stats.packageStats.remove(removedPackage);
779             }
780         }
781 
782         // filter out events
783         for (int i = stats.events.size() - 1; i >= 0; i--) {
784             final UsageEvents.Event event = stats.events.get(i);
785             final Long timeRemoved = removedPackagesMap.get(event.mPackage);
786             if (timeRemoved != null && timeRemoved > event.mTimeStamp) {
787                 stats.events.remove(i);
788             }
789         }
790     }
791 
792     /**
793      * Figures out what to extract from the given IntervalStats object.
794      */
795     public interface StatCombiner<T> {
796 
797         /**
798          * Implementations should extract interesting information from <code>stats</code> and add it
799          * to the <code>accumulatedResult</code> list.
800          *
801          * If the <code>stats</code> object is mutable, <code>mutable</code> will be true,
802          * which means you should make a copy of the data before adding it to the
803          * <code>accumulatedResult</code> list.
804          *
805          * @param stats             The {@link IntervalStats} object selected.
806          * @param mutable           Whether or not the data inside the stats object is mutable.
807          * @param accumulatedResult The list to which to add extracted data.
808          * @return Whether or not to continue providing new stats to this combiner. If {@code false}
809          * is returned, then combine will no longer be called.
810          */
combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult)811         boolean combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult);
812     }
813 
814     /**
815      * Find all {@link IntervalStats} for the given range and interval type.
816      */
817     @Nullable
queryUsageStats(int intervalType, long beginTime, long endTime, StatCombiner<T> combiner)818     public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime,
819             StatCombiner<T> combiner) {
820         // mIntervalDirs is final. Accessing its size without holding the lock should be fine.
821         if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
822             throw new IllegalArgumentException("Bad interval type " + intervalType);
823         }
824 
825         if (endTime <= beginTime) {
826             if (DEBUG) {
827                 Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")");
828             }
829             return null;
830         }
831 
832         synchronized (mLock) {
833             final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];
834 
835             int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
836             if (endIndex < 0) {
837                 // All the stats start after this range ends, so nothing matches.
838                 if (DEBUG) {
839                     Slog.d(TAG, "No results for this range. All stats start after.");
840                 }
841                 return null;
842             }
843 
844             if (intervalStats.keyAt(endIndex) == endTime) {
845                 // The endTime is exclusive, so if we matched exactly take the one before.
846                 endIndex--;
847                 if (endIndex < 0) {
848                     // All the stats start after this range ends, so nothing matches.
849                     if (DEBUG) {
850                         Slog.d(TAG, "No results for this range. All stats start after.");
851                     }
852                     return null;
853                 }
854             }
855 
856             int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
857             if (startIndex < 0) {
858                 // All the stats available have timestamps after beginTime, which means they all
859                 // match.
860                 startIndex = 0;
861             }
862 
863             final ArrayList<T> results = new ArrayList<>();
864             for (int i = startIndex; i <= endIndex; i++) {
865                 final AtomicFile f = intervalStats.valueAt(i);
866                 final IntervalStats stats = new IntervalStats();
867 
868                 if (DEBUG) {
869                     Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
870                 }
871 
872                 try {
873                     readLocked(f, stats);
874                     if (beginTime < stats.endTime
875                             && !combiner.combine(stats, false, results)) {
876                         break;
877                     }
878                 } catch (Exception e) {
879                     Slog.e(TAG, "Failed to read usage stats file", e);
880                     // We continue so that we return results that are not
881                     // corrupt.
882                 }
883             }
884             return results;
885         }
886     }
887 
888     /**
889      * Find the interval that best matches this range.
890      *
891      * TODO(adamlesinski): Use endTimeStamp in best fit calculation.
892      */
findBestFitBucket(long beginTimeStamp, long endTimeStamp)893     public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) {
894         synchronized (mLock) {
895             int bestBucket = -1;
896             long smallestDiff = Long.MAX_VALUE;
897             for (int i = mSortedStatFiles.length - 1; i >= 0; i--) {
898                 final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp);
899                 int size = mSortedStatFiles[i].size();
900                 if (index >= 0 && index < size) {
901                     // We have some results here, check if they are better than our current match.
902                     long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp);
903                     if (diff < smallestDiff) {
904                         smallestDiff = diff;
905                         bestBucket = i;
906                     }
907                 }
908             }
909             return bestBucket;
910         }
911     }
912 
913     /**
914      * Remove any usage stat files that are too old.
915      */
prune(final long currentTimeMillis)916     public void prune(final long currentTimeMillis) {
917         synchronized (mLock) {
918             mCal.setTimeInMillis(currentTimeMillis);
919             mCal.addYears(-3);
920             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY],
921                     mCal.getTimeInMillis());
922 
923             mCal.setTimeInMillis(currentTimeMillis);
924             mCal.addMonths(-6);
925             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
926                     mCal.getTimeInMillis());
927 
928             mCal.setTimeInMillis(currentTimeMillis);
929             mCal.addWeeks(-4);
930             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
931                     mCal.getTimeInMillis());
932 
933             mCal.setTimeInMillis(currentTimeMillis);
934             mCal.addDays(-10);
935             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
936                     mCal.getTimeInMillis());
937 
938             mCal.setTimeInMillis(currentTimeMillis);
939             mCal.addDays(-SELECTION_LOG_RETENTION_LEN);
940             for (int i = 0; i < mIntervalDirs.length; ++i) {
941                 pruneChooserCountsOlderThan(mIntervalDirs[i], mCal.getTimeInMillis());
942             }
943 
944             // We must re-index our file list or we will be trying to read
945             // deleted files.
946             indexFilesLocked();
947         }
948     }
949 
pruneFilesOlderThan(File dir, long expiryTime)950     private static void pruneFilesOlderThan(File dir, long expiryTime) {
951         File[] files = dir.listFiles();
952         if (files != null) {
953             for (File f : files) {
954                 long beginTime;
955                 try {
956                     beginTime = parseBeginTime(f);
957                 } catch (IOException e) {
958                     beginTime = 0;
959                 }
960 
961                 if (beginTime < expiryTime) {
962                     new AtomicFile(f).delete();
963                 }
964             }
965         }
966     }
967 
pruneChooserCountsOlderThan(File dir, long expiryTime)968     private void pruneChooserCountsOlderThan(File dir, long expiryTime) {
969         File[] files = dir.listFiles();
970         if (files != null) {
971             for (File f : files) {
972                 long beginTime;
973                 try {
974                     beginTime = parseBeginTime(f);
975                 } catch (IOException e) {
976                     beginTime = 0;
977                 }
978 
979                 if (beginTime < expiryTime) {
980                     try {
981                         final AtomicFile af = new AtomicFile(f);
982                         final IntervalStats stats = new IntervalStats();
983                         readLocked(af, stats);
984                         final int pkgCount = stats.packageStats.size();
985                         for (int i = 0; i < pkgCount; i++) {
986                             UsageStats pkgStats = stats.packageStats.valueAt(i);
987                             if (pkgStats.mChooserCounts != null) {
988                                 pkgStats.mChooserCounts.clear();
989                             }
990                         }
991                         writeLocked(af, stats);
992                     } catch (Exception e) {
993                         Slog.e(TAG, "Failed to delete chooser counts from usage stats file", e);
994                     }
995                 }
996             }
997         }
998     }
999 
parseBeginTime(AtomicFile file)1000     private static long parseBeginTime(AtomicFile file) throws IOException {
1001         return parseBeginTime(file.getBaseFile());
1002     }
1003 
parseBeginTime(File file)1004     private static long parseBeginTime(File file) throws IOException {
1005         String name = file.getName();
1006 
1007         // Parse out the digits from the the front of the file name
1008         for (int i = 0; i < name.length(); i++) {
1009             final char c = name.charAt(i);
1010             if (c < '0' || c > '9') {
1011                 // found first char that is not a digit.
1012                 name = name.substring(0, i);
1013                 break;
1014             }
1015         }
1016 
1017         try {
1018             return Long.parseLong(name);
1019         } catch (NumberFormatException e) {
1020             throw new IOException(e);
1021         }
1022     }
1023 
writeLocked(AtomicFile file, IntervalStats stats)1024     private void writeLocked(AtomicFile file, IntervalStats stats)
1025             throws IOException, RuntimeException {
1026         if (mCurrentVersion <= 3) {
1027             Slog.wtf(TAG, "Attempting to write UsageStats as XML with version " + mCurrentVersion);
1028             return;
1029         }
1030         writeLocked(file, stats, mCurrentVersion, mPackagesTokenData);
1031     }
1032 
writeLocked(AtomicFile file, IntervalStats stats, int version, PackagesTokenData packagesTokenData)1033     private static void writeLocked(AtomicFile file, IntervalStats stats, int version,
1034             PackagesTokenData packagesTokenData) throws IOException, RuntimeException {
1035         FileOutputStream fos = file.startWrite();
1036         try {
1037             writeLocked(fos, stats, version, packagesTokenData);
1038             file.finishWrite(fos);
1039             fos = null;
1040         } catch (Exception e) {
1041             // Do nothing. Exception has already been handled.
1042         } finally {
1043             // When fos is null (successful write), this will no-op
1044             file.failWrite(fos);
1045         }
1046     }
1047 
writeLocked(OutputStream out, IntervalStats stats, int version, PackagesTokenData packagesTokenData)1048     private static void writeLocked(OutputStream out, IntervalStats stats, int version,
1049             PackagesTokenData packagesTokenData) throws Exception {
1050         switch (version) {
1051             case 1:
1052             case 2:
1053             case 3:
1054                 Slog.wtf(TAG, "Attempting to write UsageStats as XML with version " + version);
1055                 break;
1056             case 4:
1057                 try {
1058                     UsageStatsProto.write(out, stats);
1059                 } catch (Exception e) {
1060                     Slog.e(TAG, "Unable to write interval stats to proto.", e);
1061                     throw e;
1062                 }
1063                 break;
1064             case 5:
1065                 stats.obfuscateData(packagesTokenData);
1066                 try {
1067                     UsageStatsProtoV2.write(out, stats);
1068                 } catch (Exception e) {
1069                     Slog.e(TAG, "Unable to write interval stats to proto.", e);
1070                     throw e;
1071                 }
1072                 break;
1073             default:
1074                 throw new RuntimeException(
1075                         "Unhandled UsageStatsDatabase version: " + Integer.toString(version)
1076                                 + " on write.");
1077         }
1078     }
1079 
1080     /**
1081      * Note: the data read from the given file will add to the IntervalStats object passed into this
1082      * method. It is up to the caller to ensure that this is the desired behavior - if not, the
1083      * caller should ensure that the data in the reused object is being cleared.
1084      */
readLocked(AtomicFile file, IntervalStats statsOut)1085     private void readLocked(AtomicFile file, IntervalStats statsOut)
1086             throws IOException, RuntimeException {
1087         if (mCurrentVersion <= 3) {
1088             Slog.wtf(TAG, "Reading UsageStats as XML; current database version: "
1089                     + mCurrentVersion);
1090         }
1091         readLocked(file, statsOut, mCurrentVersion, mPackagesTokenData);
1092     }
1093 
1094     /**
1095      * Returns {@code true} if any stats were omitted while reading, {@code false} otherwise.
1096      * <p/>
1097      * Note: the data read from the given file will add to the IntervalStats object passed into this
1098      * method. It is up to the caller to ensure that this is the desired behavior - if not, the
1099      * caller should ensure that the data in the reused object is being cleared.
1100      */
readLocked(AtomicFile file, IntervalStats statsOut, int version, PackagesTokenData packagesTokenData)1101     private static boolean readLocked(AtomicFile file, IntervalStats statsOut, int version,
1102             PackagesTokenData packagesTokenData) throws IOException, RuntimeException {
1103         boolean dataOmitted = false;
1104         try {
1105             FileInputStream in = file.openRead();
1106             try {
1107                 statsOut.beginTime = parseBeginTime(file);
1108                 dataOmitted = readLocked(in, statsOut, version, packagesTokenData);
1109                 statsOut.lastTimeSaved = file.getLastModifiedTime();
1110             } finally {
1111                 try {
1112                     in.close();
1113                 } catch (IOException e) {
1114                     // Empty
1115                 }
1116             }
1117         } catch (FileNotFoundException e) {
1118             Slog.e(TAG, "UsageStatsDatabase", e);
1119             throw e;
1120         }
1121         return dataOmitted;
1122     }
1123 
1124     /**
1125      * Returns {@code true} if any stats were omitted while reading, {@code false} otherwise.
1126      * <p/>
1127      * Note: the data read from the given file will add to the IntervalStats object passed into this
1128      * method. It is up to the caller to ensure that this is the desired behavior - if not, the
1129      * caller should ensure that the data in the reused object is being cleared.
1130      */
readLocked(InputStream in, IntervalStats statsOut, int version, PackagesTokenData packagesTokenData)1131     private static boolean readLocked(InputStream in, IntervalStats statsOut, int version,
1132             PackagesTokenData packagesTokenData) throws RuntimeException {
1133         boolean dataOmitted = false;
1134         switch (version) {
1135             case 1:
1136             case 2:
1137             case 3:
1138                 Slog.w(TAG, "Reading UsageStats as XML; database version: " + version);
1139                 try {
1140                     UsageStatsXml.read(in, statsOut);
1141                 } catch (Exception e) {
1142                     Slog.e(TAG, "Unable to read interval stats from XML", e);
1143                 }
1144                 break;
1145             case 4:
1146                 try {
1147                     UsageStatsProto.read(in, statsOut);
1148                 } catch (Exception e) {
1149                     Slog.e(TAG, "Unable to read interval stats from proto.", e);
1150                 }
1151                 break;
1152             case 5:
1153                 try {
1154                     UsageStatsProtoV2.read(in, statsOut);
1155                 } catch (Exception e) {
1156                     Slog.e(TAG, "Unable to read interval stats from proto.", e);
1157                 }
1158                 dataOmitted = statsOut.deobfuscateData(packagesTokenData);
1159                 break;
1160             default:
1161                 throw new RuntimeException(
1162                         "Unhandled UsageStatsDatabase version: " + Integer.toString(version)
1163                                 + " on read.");
1164         }
1165         return dataOmitted;
1166     }
1167 
1168     /**
1169      * Reads the obfuscated data file from disk containing the tokens to packages mappings and
1170      * rebuilds the packages to tokens mappings based on that data.
1171      */
readMappingsLocked()1172     public void readMappingsLocked() {
1173         if (!mPackageMappingsFile.exists()) {
1174             return; // package mappings file is missing - recreate mappings on next write.
1175         }
1176 
1177         try (FileInputStream in = new AtomicFile(mPackageMappingsFile).openRead()) {
1178             UsageStatsProtoV2.readObfuscatedData(in, mPackagesTokenData);
1179         } catch (Exception e) {
1180             Slog.e(TAG, "Failed to read the obfuscated packages mapping file.", e);
1181             return;
1182         }
1183 
1184         final SparseArray<ArrayList<String>> tokensToPackagesMap =
1185                 mPackagesTokenData.tokensToPackagesMap;
1186         final int tokensToPackagesMapSize = tokensToPackagesMap.size();
1187         for (int i = 0; i < tokensToPackagesMapSize; i++) {
1188             final int packageToken = tokensToPackagesMap.keyAt(i);
1189             final ArrayList<String> tokensMap = tokensToPackagesMap.valueAt(i);
1190             final ArrayMap<String, Integer> packageStringsMap = new ArrayMap<>();
1191             final int tokensMapSize = tokensMap.size();
1192             // package name will always be at index 0 but its token should not be 0
1193             packageStringsMap.put(tokensMap.get(0), packageToken);
1194             for (int j = 1; j < tokensMapSize; j++) {
1195                 packageStringsMap.put(tokensMap.get(j), j);
1196             }
1197             mPackagesTokenData.packagesToTokensMap.put(tokensMap.get(0), packageStringsMap);
1198         }
1199     }
1200 
writeMappingsLocked()1201     void writeMappingsLocked() throws IOException {
1202         final AtomicFile file = new AtomicFile(mPackageMappingsFile);
1203         FileOutputStream fos = file.startWrite();
1204         try {
1205             UsageStatsProtoV2.writeObfuscatedData(fos, mPackagesTokenData);
1206             file.finishWrite(fos);
1207             fos = null;
1208         } catch (Exception e) {
1209             Slog.e(TAG, "Unable to write obfuscated data to proto.", e);
1210         } finally {
1211             file.failWrite(fos);
1212         }
1213     }
1214 
obfuscateCurrentStats(IntervalStats[] currentStats)1215     void obfuscateCurrentStats(IntervalStats[] currentStats) {
1216         if (mCurrentVersion < 5) {
1217             return;
1218         }
1219         for (int i = 0; i < currentStats.length; i++) {
1220             final IntervalStats stats = currentStats[i];
1221             stats.obfuscateData(mPackagesTokenData);
1222         }
1223     }
1224 
1225     /**
1226      * Update the stats in the database. They may not be written to disk immediately.
1227      */
putUsageStats(int intervalType, IntervalStats stats)1228     public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
1229         if (stats == null) return;
1230         synchronized (mLock) {
1231             if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
1232                 throw new IllegalArgumentException("Bad interval type " + intervalType);
1233             }
1234 
1235             AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
1236             if (f == null) {
1237                 f = new AtomicFile(new File(mIntervalDirs[intervalType],
1238                         Long.toString(stats.beginTime)));
1239                 mSortedStatFiles[intervalType].put(stats.beginTime, f);
1240             }
1241 
1242             writeLocked(f, stats);
1243             stats.lastTimeSaved = f.getLastModifiedTime();
1244         }
1245     }
1246 
1247     /* Backup/Restore Code */
getBackupPayload(String key)1248     byte[] getBackupPayload(String key) {
1249         return getBackupPayload(key, BACKUP_VERSION);
1250     }
1251 
1252     /**
1253      * @hide
1254      */
1255     @VisibleForTesting
getBackupPayload(String key, int version)1256     public byte[] getBackupPayload(String key, int version) {
1257         if (version >= 1 && version <= 3) {
1258             Slog.wtf(TAG, "Attempting to backup UsageStats as XML with version " + version);
1259             return null;
1260         }
1261         if (version < 1 || version > BACKUP_VERSION) {
1262             Slog.wtf(TAG, "Attempting to backup UsageStats with an unknown version: " + version);
1263             return null;
1264         }
1265         synchronized (mLock) {
1266             ByteArrayOutputStream baos = new ByteArrayOutputStream();
1267             if (KEY_USAGE_STATS.equals(key)) {
1268                 prune(System.currentTimeMillis());
1269                 DataOutputStream out = new DataOutputStream(baos);
1270                 try {
1271                     out.writeInt(version);
1272 
1273                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size());
1274 
1275                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size();
1276                             i++) {
1277                         writeIntervalStatsToStream(out,
1278                                 mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i),
1279                                 version);
1280                     }
1281 
1282                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size());
1283                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size();
1284                             i++) {
1285                         writeIntervalStatsToStream(out,
1286                                 mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i),
1287                                 version);
1288                     }
1289 
1290                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size());
1291                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size();
1292                             i++) {
1293                         writeIntervalStatsToStream(out,
1294                                 mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i),
1295                                 version);
1296                     }
1297 
1298                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size());
1299                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size();
1300                             i++) {
1301                         writeIntervalStatsToStream(out,
1302                                 mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i),
1303                                 version);
1304                     }
1305                     if (DEBUG) Slog.i(TAG, "Written " + baos.size() + " bytes of data");
1306                 } catch (IOException ioe) {
1307                     Slog.d(TAG, "Failed to write data to output stream", ioe);
1308                     baos.reset();
1309                 }
1310             }
1311             return baos.toByteArray();
1312         }
1313     }
1314 
1315     /**
1316      * Updates the set of packages given to only include those that have been used within the
1317      * given timeframe (as defined by {@link UsageStats#getLastTimePackageUsed()}).
1318      */
calculatePackagesUsedWithinTimeframe( IntervalStats stats, Set<String> packagesList, long timeframeMs)1319     private void calculatePackagesUsedWithinTimeframe(
1320             IntervalStats stats, Set<String> packagesList, long timeframeMs) {
1321         for (UsageStats stat : stats.packageStats.values()) {
1322             if (stat.getLastTimePackageUsed() > timeframeMs) {
1323                 packagesList.add(stat.mPackageName);
1324             }
1325         }
1326     }
1327 
1328     /**
1329      * @hide
1330      */
1331     @VisibleForTesting
applyRestoredPayload(String key, byte[] payload)1332     public @NonNull Set<String> applyRestoredPayload(String key, byte[] payload) {
1333         synchronized (mLock) {
1334             if (KEY_USAGE_STATS.equals(key)) {
1335                 // Read stats files for the current device configs
1336                 IntervalStats dailyConfigSource =
1337                         getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY);
1338                 IntervalStats weeklyConfigSource =
1339                         getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY);
1340                 IntervalStats monthlyConfigSource =
1341                         getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY);
1342                 IntervalStats yearlyConfigSource =
1343                         getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);
1344 
1345                 final Set<String> packagesRestored = new ArraySet<>();
1346                 try {
1347                     DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
1348                     int backupDataVersion = in.readInt();
1349 
1350                     // Can't handle this backup set
1351                     if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) {
1352                         return packagesRestored;
1353                     }
1354 
1355                     // Delete all stats files
1356                     // Do this after reading version and before actually restoring
1357                     for (int i = 0; i < mIntervalDirs.length; i++) {
1358                         deleteDirectoryContents(mIntervalDirs[i]);
1359                     }
1360 
1361                     // 90 days before today in epoch
1362                     final long timeframe = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(90);
1363                     int fileCount = in.readInt();
1364                     for (int i = 0; i < fileCount; i++) {
1365                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
1366                                 backupDataVersion);
1367                         calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
1368                         packagesRestored.addAll(stats.packageStats.keySet());
1369                         stats = mergeStats(stats, dailyConfigSource);
1370                         putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats);
1371                     }
1372 
1373                     fileCount = in.readInt();
1374                     for (int i = 0; i < fileCount; i++) {
1375                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
1376                                 backupDataVersion);
1377                         calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
1378                         stats = mergeStats(stats, weeklyConfigSource);
1379                         putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats);
1380                     }
1381 
1382                     fileCount = in.readInt();
1383                     for (int i = 0; i < fileCount; i++) {
1384                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
1385                                 backupDataVersion);
1386                         calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
1387                         stats = mergeStats(stats, monthlyConfigSource);
1388                         putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats);
1389                     }
1390 
1391                     fileCount = in.readInt();
1392                     for (int i = 0; i < fileCount; i++) {
1393                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
1394                                 backupDataVersion);
1395                         calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
1396                         stats = mergeStats(stats, yearlyConfigSource);
1397                         putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats);
1398                     }
1399                     if (DEBUG) Slog.i(TAG, "Completed Restoring UsageStats");
1400                 } catch (IOException ioe) {
1401                     Slog.d(TAG, "Failed to read data from input stream", ioe);
1402                 } finally {
1403                     indexFilesLocked();
1404                 }
1405                 return packagesRestored;
1406             }
1407             return Collections.EMPTY_SET;
1408         }
1409     }
1410 
1411     /**
1412      * Get the Configuration Statistics from the current device statistics and merge them
1413      * with the backed up usage statistics.
1414      */
mergeStats(IntervalStats beingRestored, IntervalStats onDevice)1415     private IntervalStats mergeStats(IntervalStats beingRestored, IntervalStats onDevice) {
1416         if (onDevice == null) return beingRestored;
1417         if (beingRestored == null) return null;
1418         beingRestored.activeConfiguration = onDevice.activeConfiguration;
1419         beingRestored.configurations.putAll(onDevice.configurations);
1420         beingRestored.events.clear();
1421         beingRestored.events.merge(onDevice.events);
1422         return beingRestored;
1423     }
1424 
writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version)1425     private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile, int version)
1426             throws IOException {
1427         IntervalStats stats = new IntervalStats();
1428         try {
1429             readLocked(statsFile, stats);
1430         } catch (IOException e) {
1431             Slog.e(TAG, "Failed to read usage stats file", e);
1432             out.writeInt(0);
1433             return;
1434         }
1435         sanitizeIntervalStatsForBackup(stats);
1436         byte[] data = serializeIntervalStats(stats, version);
1437         out.writeInt(data.length);
1438         out.write(data);
1439     }
1440 
getIntervalStatsBytes(DataInputStream in)1441     private static byte[] getIntervalStatsBytes(DataInputStream in) throws IOException {
1442         int length = in.readInt();
1443         byte[] buffer = new byte[length];
1444         in.read(buffer, 0, length);
1445         return buffer;
1446     }
1447 
sanitizeIntervalStatsForBackup(IntervalStats stats)1448     private static void sanitizeIntervalStatsForBackup(IntervalStats stats) {
1449         if (stats == null) return;
1450         stats.activeConfiguration = null;
1451         stats.configurations.clear();
1452         stats.events.clear();
1453     }
1454 
serializeIntervalStats(IntervalStats stats, int version)1455     private byte[] serializeIntervalStats(IntervalStats stats, int version) {
1456         ByteArrayOutputStream baos = new ByteArrayOutputStream();
1457         DataOutputStream out = new DataOutputStream(baos);
1458         try {
1459             out.writeLong(stats.beginTime);
1460             writeLocked(out, stats, version, mPackagesTokenData);
1461         } catch (Exception ioe) {
1462             Slog.d(TAG, "Serializing IntervalStats Failed", ioe);
1463             baos.reset();
1464         }
1465         return baos.toByteArray();
1466     }
1467 
deserializeIntervalStats(byte[] data, int version)1468     private IntervalStats deserializeIntervalStats(byte[] data, int version) {
1469         ByteArrayInputStream bais = new ByteArrayInputStream(data);
1470         DataInputStream in = new DataInputStream(bais);
1471         IntervalStats stats = new IntervalStats();
1472         try {
1473             stats.beginTime = in.readLong();
1474             readLocked(in, stats, version, mPackagesTokenData);
1475         } catch (Exception e) {
1476             Slog.d(TAG, "DeSerializing IntervalStats Failed", e);
1477             stats = null;
1478         }
1479         return stats;
1480     }
1481 
deleteDirectoryContents(File directory)1482     private static void deleteDirectoryContents(File directory) {
1483         File[] files = directory.listFiles();
1484         for (File file : files) {
1485             deleteDirectory(file);
1486         }
1487     }
1488 
deleteDirectory(File directory)1489     private static void deleteDirectory(File directory) {
1490         File[] files = directory.listFiles();
1491         if (files != null) {
1492             for (File file : files) {
1493                 if (!file.isDirectory()) {
1494                     file.delete();
1495                 } else {
1496                     deleteDirectory(file);
1497                 }
1498             }
1499         }
1500         directory.delete();
1501     }
1502 
1503     /**
1504      * Prints the obfuscated package mappings and a summary of the database files.
1505      * @param pw the print writer to print to
1506      */
dump(IndentingPrintWriter pw, boolean compact)1507     public void dump(IndentingPrintWriter pw, boolean compact) {
1508         synchronized (mLock) {
1509             pw.println();
1510             pw.println("UsageStatsDatabase:");
1511             pw.increaseIndent();
1512             dumpMappings(pw);
1513             pw.decreaseIndent();
1514             pw.println("Database Summary:");
1515             pw.increaseIndent();
1516             for (int i = 0; i < mSortedStatFiles.length; i++) {
1517                 final TimeSparseArray<AtomicFile> files = mSortedStatFiles[i];
1518                 final int size = files.size();
1519                 pw.print(UserUsageStatsService.intervalToString(i));
1520                 pw.print(" stats files: ");
1521                 pw.print(size);
1522                 pw.println(", sorted list of files:");
1523                 pw.increaseIndent();
1524                 for (int f = 0; f < size; f++) {
1525                     final long fileName = files.keyAt(f);
1526                     if (compact) {
1527                         pw.print(UserUsageStatsService.formatDateTime(fileName, false));
1528                     } else {
1529                         pw.printPair(Long.toString(fileName),
1530                                 UserUsageStatsService.formatDateTime(fileName, true));
1531                     }
1532                     pw.println();
1533                 }
1534                 pw.decreaseIndent();
1535             }
1536             pw.decreaseIndent();
1537         }
1538     }
1539 
dumpMappings(IndentingPrintWriter pw)1540     void dumpMappings(IndentingPrintWriter pw) {
1541         synchronized (mLock) {
1542             pw.println("Obfuscated Packages Mappings:");
1543             pw.increaseIndent();
1544             pw.println("Counter: " + mPackagesTokenData.counter);
1545             pw.println("Tokens Map Size: " + mPackagesTokenData.tokensToPackagesMap.size());
1546             if (!mPackagesTokenData.removedPackageTokens.isEmpty()) {
1547                 pw.println("Removed Package Tokens: "
1548                         + Arrays.toString(mPackagesTokenData.removedPackageTokens.toArray()));
1549             }
1550             for (int i = 0; i < mPackagesTokenData.tokensToPackagesMap.size(); i++) {
1551                 final int packageToken = mPackagesTokenData.tokensToPackagesMap.keyAt(i);
1552                 final String packageStrings = String.join(", ",
1553                         mPackagesTokenData.tokensToPackagesMap.valueAt(i));
1554                 pw.println("Token " + packageToken + ": [" + packageStrings + "]");
1555             }
1556             pw.println();
1557             pw.decreaseIndent();
1558         }
1559     }
1560 
deleteDataFor(String pkg)1561     void deleteDataFor(String pkg) {
1562         // reuse the existing prune method to delete data for the specified package.
1563         // we'll use the current timestamp so that all events before now get pruned.
1564         prunePackagesDataOnUpgrade(
1565                 new HashMap<>(Collections.singletonMap(pkg, SystemClock.elapsedRealtime())));
1566     }
1567 
readIntervalStatsForFile(int interval, long fileName)1568     IntervalStats readIntervalStatsForFile(int interval, long fileName) {
1569         synchronized (mLock) {
1570             final IntervalStats stats = new IntervalStats();
1571             try {
1572                 readLocked(mSortedStatFiles[interval].get(fileName, null), stats);
1573                 return stats;
1574             } catch (Exception e) {
1575                 return null;
1576             }
1577         }
1578     }
1579 }
1580