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