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