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