1 /**
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy
6  * of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations
14  * under the License.
15  */
16 
17 package com.android.server.usage;
18 
19 import android.app.usage.TimeSparseArray;
20 import android.app.usage.UsageStats;
21 import android.app.usage.UsageStatsManager;
22 import android.os.Build;
23 import android.os.SystemProperties;
24 import android.util.AtomicFile;
25 import android.util.Slog;
26 import android.util.TimeUtils;
27 
28 import java.io.BufferedReader;
29 import java.io.BufferedWriter;
30 import java.io.ByteArrayInputStream;
31 import java.io.ByteArrayOutputStream;
32 import java.io.DataInputStream;
33 import java.io.DataOutputStream;
34 import java.io.File;
35 import java.io.FileReader;
36 import java.io.FileWriter;
37 import java.io.FilenameFilter;
38 import java.io.IOException;
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * Provides an interface to query for UsageStat data from an XML database.
44  */
45 class UsageStatsDatabase {
46     private static final int CURRENT_VERSION = 3;
47 
48     // Current version of the backup schema
49     static final int BACKUP_VERSION = 1;
50 
51     // Key under which the payload blob is stored
52     // same as UsageStatsBackupHelper.KEY_USAGE_STATS
53     static final String KEY_USAGE_STATS = "usage_stats";
54 
55 
56     private static final String TAG = "UsageStatsDatabase";
57     private static final boolean DEBUG = UsageStatsService.DEBUG;
58     private static final String BAK_SUFFIX = ".bak";
59     private static final String CHECKED_IN_SUFFIX = UsageStatsXml.CHECKED_IN_SUFFIX;
60     private static final String RETENTION_LEN_KEY = "ro.usagestats.chooser.retention";
61     private static final int SELECTION_LOG_RETENTION_LEN =
62             SystemProperties.getInt(RETENTION_LEN_KEY, 14);
63 
64     private final Object mLock = new Object();
65     private final File[] mIntervalDirs;
66     private final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
67     private final UnixCalendar mCal;
68     private final File mVersionFile;
69     private boolean mFirstUpdate;
70     private boolean mNewUpdate;
71 
UsageStatsDatabase(File dir)72     public UsageStatsDatabase(File dir) {
73         mIntervalDirs = new File[] {
74                 new File(dir, "daily"),
75                 new File(dir, "weekly"),
76                 new File(dir, "monthly"),
77                 new File(dir, "yearly"),
78         };
79         mVersionFile = new File(dir, "version");
80         mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
81         mCal = new UnixCalendar(0);
82     }
83 
84     /**
85      * Initialize any directories required and index what stats are available.
86      */
init(long currentTimeMillis)87     public void init(long currentTimeMillis) {
88         synchronized (mLock) {
89             for (File f : mIntervalDirs) {
90                 f.mkdirs();
91                 if (!f.exists()) {
92                     throw new IllegalStateException("Failed to create directory "
93                             + f.getAbsolutePath());
94                 }
95             }
96 
97             checkVersionAndBuildLocked();
98             indexFilesLocked();
99 
100             // Delete files that are in the future.
101             for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) {
102                 final int startIndex = files.closestIndexOnOrAfter(currentTimeMillis);
103                 if (startIndex < 0) {
104                     continue;
105                 }
106 
107                 final int fileCount = files.size();
108                 for (int i = startIndex; i < fileCount; i++) {
109                     files.valueAt(i).delete();
110                 }
111 
112                 // Remove in a separate loop because any accesses (valueAt)
113                 // will cause a gc in the SparseArray and mess up the order.
114                 for (int i = startIndex; i < fileCount; i++) {
115                     files.removeAt(i);
116                 }
117             }
118         }
119     }
120 
121     public interface CheckinAction {
checkin(IntervalStats stats)122         boolean checkin(IntervalStats stats);
123     }
124 
125     /**
126      * Calls {@link CheckinAction#checkin(IntervalStats)} on the given {@link CheckinAction}
127      * for all {@link IntervalStats} that haven't been checked-in.
128      * If any of the calls to {@link CheckinAction#checkin(IntervalStats)} returns false or throws
129      * an exception, the check-in will be aborted.
130      *
131      * @param checkinAction The callback to run when checking-in {@link IntervalStats}.
132      * @return true if the check-in succeeded.
133      */
checkinDailyFiles(CheckinAction checkinAction)134     public boolean checkinDailyFiles(CheckinAction checkinAction) {
135         synchronized (mLock) {
136             final TimeSparseArray<AtomicFile> files =
137                     mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY];
138             final int fileCount = files.size();
139 
140             // We may have holes in the checkin (if there was an error)
141             // so find the last checked-in file and go from there.
142             int lastCheckin = -1;
143             for (int i = 0; i < fileCount - 1; i++) {
144                 if (files.valueAt(i).getBaseFile().getPath().endsWith(CHECKED_IN_SUFFIX)) {
145                     lastCheckin = i;
146                 }
147             }
148 
149             final int start = lastCheckin + 1;
150             if (start == fileCount - 1) {
151                 return true;
152             }
153 
154             try {
155                 IntervalStats stats = new IntervalStats();
156                 for (int i = start; i < fileCount - 1; i++) {
157                     UsageStatsXml.read(files.valueAt(i), stats);
158                     if (!checkinAction.checkin(stats)) {
159                         return false;
160                     }
161                 }
162             } catch (IOException e) {
163                 Slog.e(TAG, "Failed to check-in", e);
164                 return false;
165             }
166 
167             // We have successfully checked-in the stats, so rename the files so that they
168             // are marked as checked-in.
169             for (int i = start; i < fileCount - 1; i++) {
170                 final AtomicFile file = files.valueAt(i);
171                 final File checkedInFile = new File(
172                         file.getBaseFile().getPath() + CHECKED_IN_SUFFIX);
173                 if (!file.getBaseFile().renameTo(checkedInFile)) {
174                     // We must return success, as we've already marked some files as checked-in.
175                     // It's better to repeat ourselves than to lose data.
176                     Slog.e(TAG, "Failed to mark file " + file.getBaseFile().getPath()
177                             + " as checked-in");
178                     return true;
179                 }
180 
181                 // AtomicFile needs to set a new backup path with the same -c extension, so
182                 // we replace the old AtomicFile with the updated one.
183                 files.setValueAt(i, new AtomicFile(checkedInFile));
184             }
185         }
186         return true;
187     }
188 
indexFilesLocked()189     private void indexFilesLocked() {
190         final FilenameFilter backupFileFilter = new FilenameFilter() {
191             @Override
192             public boolean accept(File dir, String name) {
193                 return !name.endsWith(BAK_SUFFIX);
194             }
195         };
196 
197         // Index the available usage stat files on disk.
198         for (int i = 0; i < mSortedStatFiles.length; i++) {
199             if (mSortedStatFiles[i] == null) {
200                 mSortedStatFiles[i] = new TimeSparseArray<>();
201             } else {
202                 mSortedStatFiles[i].clear();
203             }
204             File[] files = mIntervalDirs[i].listFiles(backupFileFilter);
205             if (files != null) {
206                 if (DEBUG) {
207                     Slog.d(TAG, "Found " + files.length + " stat files for interval " + i);
208                 }
209 
210                 for (File f : files) {
211                     final AtomicFile af = new AtomicFile(f);
212                     try {
213                         mSortedStatFiles[i].put(UsageStatsXml.parseBeginTime(af), af);
214                     } catch (IOException e) {
215                         Slog.e(TAG, "failed to index file: " + f, e);
216                     }
217                 }
218             }
219         }
220     }
221 
222     /**
223      * Is this the first update to the system from L to M?
224      */
isFirstUpdate()225     boolean isFirstUpdate() {
226         return mFirstUpdate;
227     }
228 
229     /**
230      * Is this a system update since we started tracking build fingerprint in the version file?
231      */
isNewUpdate()232     boolean isNewUpdate() {
233         return mNewUpdate;
234     }
235 
checkVersionAndBuildLocked()236     private void checkVersionAndBuildLocked() {
237         int version;
238         String buildFingerprint;
239         String currentFingerprint = getBuildFingerprint();
240         mFirstUpdate = true;
241         mNewUpdate = true;
242         try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
243             version = Integer.parseInt(reader.readLine());
244             buildFingerprint = reader.readLine();
245             if (buildFingerprint != null) {
246                 mFirstUpdate = false;
247             }
248             if (currentFingerprint.equals(buildFingerprint)) {
249                 mNewUpdate = false;
250             }
251         } catch (NumberFormatException | IOException e) {
252             version = 0;
253         }
254 
255         if (version != CURRENT_VERSION) {
256             Slog.i(TAG, "Upgrading from version " + version + " to " + CURRENT_VERSION);
257             doUpgradeLocked(version);
258         }
259 
260         if (version != CURRENT_VERSION || mNewUpdate) {
261             try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
262                 writer.write(Integer.toString(CURRENT_VERSION));
263                 writer.write("\n");
264                 writer.write(currentFingerprint);
265                 writer.write("\n");
266                 writer.flush();
267             } catch (IOException e) {
268                 Slog.e(TAG, "Failed to write new version");
269                 throw new RuntimeException(e);
270             }
271         }
272     }
273 
getBuildFingerprint()274     private String getBuildFingerprint() {
275         return Build.VERSION.RELEASE + ";"
276                 + Build.VERSION.CODENAME + ";"
277                 + Build.VERSION.INCREMENTAL;
278     }
279 
doUpgradeLocked(int thisVersion)280     private void doUpgradeLocked(int thisVersion) {
281         if (thisVersion < 2) {
282             // Delete all files if we are version 0. This is a pre-release version,
283             // so this is fine.
284             Slog.i(TAG, "Deleting all usage stats files");
285             for (int i = 0; i < mIntervalDirs.length; i++) {
286                 File[] files = mIntervalDirs[i].listFiles();
287                 if (files != null) {
288                     for (File f : files) {
289                         f.delete();
290                     }
291                 }
292             }
293         }
294     }
295 
onTimeChanged(long timeDiffMillis)296     public void onTimeChanged(long timeDiffMillis) {
297         synchronized (mLock) {
298             StringBuilder logBuilder = new StringBuilder();
299             logBuilder.append("Time changed by ");
300             TimeUtils.formatDuration(timeDiffMillis, logBuilder);
301             logBuilder.append(".");
302 
303             int filesDeleted = 0;
304             int filesMoved = 0;
305 
306             for (TimeSparseArray<AtomicFile> files : mSortedStatFiles) {
307                 final int fileCount = files.size();
308                 for (int i = 0; i < fileCount; i++) {
309                     final AtomicFile file = files.valueAt(i);
310                     final long newTime = files.keyAt(i) + timeDiffMillis;
311                     if (newTime < 0) {
312                         filesDeleted++;
313                         file.delete();
314                     } else {
315                         try {
316                             file.openRead().close();
317                         } catch (IOException e) {
318                             // Ignore, this is just to make sure there are no backups.
319                         }
320 
321                         String newName = Long.toString(newTime);
322                         if (file.getBaseFile().getName().endsWith(CHECKED_IN_SUFFIX)) {
323                             newName = newName + CHECKED_IN_SUFFIX;
324                         }
325 
326                         final File newFile = new File(file.getBaseFile().getParentFile(), newName);
327                         filesMoved++;
328                         file.getBaseFile().renameTo(newFile);
329                     }
330                 }
331                 files.clear();
332             }
333 
334             logBuilder.append(" files deleted: ").append(filesDeleted);
335             logBuilder.append(" files moved: ").append(filesMoved);
336             Slog.i(TAG, logBuilder.toString());
337 
338             // Now re-index the new files.
339             indexFilesLocked();
340         }
341     }
342 
343     /**
344      * Get the latest stats that exist for this interval type.
345      */
getLatestUsageStats(int intervalType)346     public IntervalStats getLatestUsageStats(int intervalType) {
347         synchronized (mLock) {
348             if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
349                 throw new IllegalArgumentException("Bad interval type " + intervalType);
350             }
351 
352             final int fileCount = mSortedStatFiles[intervalType].size();
353             if (fileCount == 0) {
354                 return null;
355             }
356 
357             try {
358                 final AtomicFile f = mSortedStatFiles[intervalType].valueAt(fileCount - 1);
359                 IntervalStats stats = new IntervalStats();
360                 UsageStatsXml.read(f, stats);
361                 return stats;
362             } catch (IOException e) {
363                 Slog.e(TAG, "Failed to read usage stats file", e);
364             }
365         }
366         return null;
367     }
368 
369     /**
370      * Figures out what to extract from the given IntervalStats object.
371      */
372     interface StatCombiner<T> {
373 
374         /**
375          * Implementations should extract interesting from <code>stats</code> and add it
376          * to the <code>accumulatedResult</code> list.
377          *
378          * If the <code>stats</code> object is mutable, <code>mutable</code> will be true,
379          * which means you should make a copy of the data before adding it to the
380          * <code>accumulatedResult</code> list.
381          *
382          * @param stats The {@link IntervalStats} object selected.
383          * @param mutable Whether or not the data inside the stats object is mutable.
384          * @param accumulatedResult The list to which to add extracted data.
385          */
combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult)386         void combine(IntervalStats stats, boolean mutable, List<T> accumulatedResult);
387     }
388 
389     /**
390      * Find all {@link IntervalStats} for the given range and interval type.
391      */
queryUsageStats(int intervalType, long beginTime, long endTime, StatCombiner<T> combiner)392     public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime,
393             StatCombiner<T> combiner) {
394         synchronized (mLock) {
395             if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
396                 throw new IllegalArgumentException("Bad interval type " + intervalType);
397             }
398 
399             final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];
400 
401             if (endTime <= beginTime) {
402                 if (DEBUG) {
403                     Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")");
404                 }
405                 return null;
406             }
407 
408             int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
409             if (startIndex < 0) {
410                 // All the stats available have timestamps after beginTime, which means they all
411                 // match.
412                 startIndex = 0;
413             }
414 
415             int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
416             if (endIndex < 0) {
417                 // All the stats start after this range ends, so nothing matches.
418                 if (DEBUG) {
419                     Slog.d(TAG, "No results for this range. All stats start after.");
420                 }
421                 return null;
422             }
423 
424             if (intervalStats.keyAt(endIndex) == endTime) {
425                 // The endTime is exclusive, so if we matched exactly take the one before.
426                 endIndex--;
427                 if (endIndex < 0) {
428                     // All the stats start after this range ends, so nothing matches.
429                     if (DEBUG) {
430                         Slog.d(TAG, "No results for this range. All stats start after.");
431                     }
432                     return null;
433                 }
434             }
435 
436             final IntervalStats stats = new IntervalStats();
437             final ArrayList<T> results = new ArrayList<>();
438             for (int i = startIndex; i <= endIndex; i++) {
439                 final AtomicFile f = intervalStats.valueAt(i);
440 
441                 if (DEBUG) {
442                     Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
443                 }
444 
445                 try {
446                     UsageStatsXml.read(f, stats);
447                     if (beginTime < stats.endTime) {
448                         combiner.combine(stats, false, results);
449                     }
450                 } catch (IOException e) {
451                     Slog.e(TAG, "Failed to read usage stats file", e);
452                     // We continue so that we return results that are not
453                     // corrupt.
454                 }
455             }
456             return results;
457         }
458     }
459 
460     /**
461      * Find the interval that best matches this range.
462      *
463      * TODO(adamlesinski): Use endTimeStamp in best fit calculation.
464      */
findBestFitBucket(long beginTimeStamp, long endTimeStamp)465     public int findBestFitBucket(long beginTimeStamp, long endTimeStamp) {
466         synchronized (mLock) {
467             int bestBucket = -1;
468             long smallestDiff = Long.MAX_VALUE;
469             for (int i = mSortedStatFiles.length - 1; i >= 0; i--) {
470                 final int index = mSortedStatFiles[i].closestIndexOnOrBefore(beginTimeStamp);
471                 int size = mSortedStatFiles[i].size();
472                 if (index >= 0 && index < size) {
473                     // We have some results here, check if they are better than our current match.
474                     long diff = Math.abs(mSortedStatFiles[i].keyAt(index) - beginTimeStamp);
475                     if (diff < smallestDiff) {
476                         smallestDiff = diff;
477                         bestBucket = i;
478                     }
479                 }
480             }
481             return bestBucket;
482         }
483     }
484 
485     /**
486      * Remove any usage stat files that are too old.
487      */
prune(final long currentTimeMillis)488     public void prune(final long currentTimeMillis) {
489         synchronized (mLock) {
490             mCal.setTimeInMillis(currentTimeMillis);
491             mCal.addYears(-3);
492             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY],
493                     mCal.getTimeInMillis());
494 
495             mCal.setTimeInMillis(currentTimeMillis);
496             mCal.addMonths(-6);
497             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
498                     mCal.getTimeInMillis());
499 
500             mCal.setTimeInMillis(currentTimeMillis);
501             mCal.addWeeks(-4);
502             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
503                     mCal.getTimeInMillis());
504 
505             mCal.setTimeInMillis(currentTimeMillis);
506             mCal.addDays(-7);
507             pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
508                     mCal.getTimeInMillis());
509 
510             mCal.setTimeInMillis(currentTimeMillis);
511             mCal.addDays(-SELECTION_LOG_RETENTION_LEN);
512             for (int i = 0; i < mIntervalDirs.length; ++i) {
513                 pruneChooserCountsOlderThan(mIntervalDirs[i], mCal.getTimeInMillis());
514             }
515 
516             // We must re-index our file list or we will be trying to read
517             // deleted files.
518             indexFilesLocked();
519         }
520     }
521 
pruneFilesOlderThan(File dir, long expiryTime)522     private static void pruneFilesOlderThan(File dir, long expiryTime) {
523         File[] files = dir.listFiles();
524         if (files != null) {
525             for (File f : files) {
526                 String path = f.getPath();
527                 if (path.endsWith(BAK_SUFFIX)) {
528                     f = new File(path.substring(0, path.length() - BAK_SUFFIX.length()));
529                 }
530 
531                 long beginTime;
532                 try {
533                     beginTime = UsageStatsXml.parseBeginTime(f);
534                 } catch (IOException e) {
535                     beginTime = 0;
536                 }
537 
538                 if (beginTime < expiryTime) {
539                     new AtomicFile(f).delete();
540                 }
541             }
542         }
543     }
544 
pruneChooserCountsOlderThan(File dir, long expiryTime)545     private static void pruneChooserCountsOlderThan(File dir, long expiryTime) {
546         File[] files = dir.listFiles();
547         if (files != null) {
548             for (File f : files) {
549                 String path = f.getPath();
550                 if (path.endsWith(BAK_SUFFIX)) {
551                     f = new File(path.substring(0, path.length() - BAK_SUFFIX.length()));
552                 }
553 
554                 long beginTime;
555                 try {
556                     beginTime = UsageStatsXml.parseBeginTime(f);
557                 } catch (IOException e) {
558                     beginTime = 0;
559                 }
560 
561                 if (beginTime < expiryTime) {
562                     try {
563                         final AtomicFile af = new AtomicFile(f);
564                         final IntervalStats stats = new IntervalStats();
565                         UsageStatsXml.read(af, stats);
566                         final int pkgCount = stats.packageStats.size();
567                         for (int i = 0; i < pkgCount; i++) {
568                             UsageStats pkgStats = stats.packageStats.valueAt(i);
569                             if (pkgStats.mChooserCounts != null) {
570                                 pkgStats.mChooserCounts.clear();
571                             }
572                         }
573                         UsageStatsXml.write(af, stats);
574                     } catch (IOException e) {
575                         Slog.e(TAG, "Failed to delete chooser counts from usage stats file", e);
576                     }
577                 }
578             }
579         }
580     }
581 
582     /**
583      * Update the stats in the database. They may not be written to disk immediately.
584      */
putUsageStats(int intervalType, IntervalStats stats)585     public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
586         if (stats == null) return;
587         synchronized (mLock) {
588             if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
589                 throw new IllegalArgumentException("Bad interval type " + intervalType);
590             }
591 
592             AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
593             if (f == null) {
594                 f = new AtomicFile(new File(mIntervalDirs[intervalType],
595                         Long.toString(stats.beginTime)));
596                 mSortedStatFiles[intervalType].put(stats.beginTime, f);
597             }
598 
599             UsageStatsXml.write(f, stats);
600             stats.lastTimeSaved = f.getLastModifiedTime();
601         }
602     }
603 
604 
605     /* Backup/Restore Code */
getBackupPayload(String key)606     byte[] getBackupPayload(String key) {
607         synchronized (mLock) {
608             ByteArrayOutputStream baos = new ByteArrayOutputStream();
609             if (KEY_USAGE_STATS.equals(key)) {
610                 prune(System.currentTimeMillis());
611                 DataOutputStream out = new DataOutputStream(baos);
612                 try {
613                     out.writeInt(BACKUP_VERSION);
614 
615                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size());
616                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].size();
617                             i++) {
618                         writeIntervalStatsToStream(out,
619                                 mSortedStatFiles[UsageStatsManager.INTERVAL_DAILY].valueAt(i));
620                     }
621 
622                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size());
623                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].size();
624                             i++) {
625                         writeIntervalStatsToStream(out,
626                                 mSortedStatFiles[UsageStatsManager.INTERVAL_WEEKLY].valueAt(i));
627                     }
628 
629                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size());
630                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].size();
631                             i++) {
632                         writeIntervalStatsToStream(out,
633                                 mSortedStatFiles[UsageStatsManager.INTERVAL_MONTHLY].valueAt(i));
634                     }
635 
636                     out.writeInt(mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size());
637                     for (int i = 0; i < mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].size();
638                             i++) {
639                         writeIntervalStatsToStream(out,
640                                 mSortedStatFiles[UsageStatsManager.INTERVAL_YEARLY].valueAt(i));
641                     }
642                     if (DEBUG) Slog.i(TAG, "Written " + baos.size() + " bytes of data");
643                 } catch (IOException ioe) {
644                     Slog.d(TAG, "Failed to write data to output stream", ioe);
645                     baos.reset();
646                 }
647             }
648             return baos.toByteArray();
649         }
650 
651     }
652 
applyRestoredPayload(String key, byte[] payload)653     void applyRestoredPayload(String key, byte[] payload) {
654         synchronized (mLock) {
655             if (KEY_USAGE_STATS.equals(key)) {
656                 // Read stats files for the current device configs
657                 IntervalStats dailyConfigSource =
658                         getLatestUsageStats(UsageStatsManager.INTERVAL_DAILY);
659                 IntervalStats weeklyConfigSource =
660                         getLatestUsageStats(UsageStatsManager.INTERVAL_WEEKLY);
661                 IntervalStats monthlyConfigSource =
662                         getLatestUsageStats(UsageStatsManager.INTERVAL_MONTHLY);
663                 IntervalStats yearlyConfigSource =
664                         getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);
665 
666                 try {
667                     DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
668                     int backupDataVersion = in.readInt();
669 
670                     // Can't handle this backup set
671                     if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return;
672 
673                     // Delete all stats files
674                     // Do this after reading version and before actually restoring
675                     for (int i = 0; i < mIntervalDirs.length; i++) {
676                         deleteDirectoryContents(mIntervalDirs[i]);
677                     }
678 
679                     int fileCount = in.readInt();
680                     for (int i = 0; i < fileCount; i++) {
681                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
682                         stats = mergeStats(stats, dailyConfigSource);
683                         putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats);
684                     }
685 
686                     fileCount = in.readInt();
687                     for (int i = 0; i < fileCount; i++) {
688                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
689                         stats = mergeStats(stats, weeklyConfigSource);
690                         putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats);
691                     }
692 
693                     fileCount = in.readInt();
694                     for (int i = 0; i < fileCount; i++) {
695                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
696                         stats = mergeStats(stats, monthlyConfigSource);
697                         putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats);
698                     }
699 
700                     fileCount = in.readInt();
701                     for (int i = 0; i < fileCount; i++) {
702                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in));
703                         stats = mergeStats(stats, yearlyConfigSource);
704                         putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats);
705                     }
706                     if (DEBUG) Slog.i(TAG, "Completed Restoring UsageStats");
707                 } catch (IOException ioe) {
708                     Slog.d(TAG, "Failed to read data from input stream", ioe);
709                 } finally {
710                     indexFilesLocked();
711                 }
712             }
713         }
714     }
715 
716     /**
717      * Get the Configuration Statistics from the current device statistics and merge them
718      * with the backed up usage statistics.
719      */
mergeStats(IntervalStats beingRestored, IntervalStats onDevice)720     private IntervalStats mergeStats(IntervalStats beingRestored, IntervalStats onDevice) {
721         if (onDevice == null) return beingRestored;
722         if (beingRestored == null) return null;
723         beingRestored.activeConfiguration = onDevice.activeConfiguration;
724         beingRestored.configurations.putAll(onDevice.configurations);
725         beingRestored.events = onDevice.events;
726         return beingRestored;
727     }
728 
writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile)729     private void writeIntervalStatsToStream(DataOutputStream out, AtomicFile statsFile)
730             throws IOException {
731         IntervalStats stats = new IntervalStats();
732         try {
733             UsageStatsXml.read(statsFile, stats);
734         } catch (IOException e) {
735             Slog.e(TAG, "Failed to read usage stats file", e);
736             out.writeInt(0);
737             return;
738         }
739         sanitizeIntervalStatsForBackup(stats);
740         byte[] data = serializeIntervalStats(stats);
741         out.writeInt(data.length);
742         out.write(data);
743     }
744 
getIntervalStatsBytes(DataInputStream in)745     private static byte[] getIntervalStatsBytes(DataInputStream in) throws IOException {
746         int length = in.readInt();
747         byte[] buffer = new byte[length];
748         in.read(buffer, 0, length);
749         return buffer;
750     }
751 
sanitizeIntervalStatsForBackup(IntervalStats stats)752     private static void sanitizeIntervalStatsForBackup(IntervalStats stats) {
753         if (stats == null) return;
754         stats.activeConfiguration = null;
755         stats.configurations.clear();
756         if (stats.events != null) stats.events.clear();
757     }
758 
serializeIntervalStats(IntervalStats stats)759     private static byte[] serializeIntervalStats(IntervalStats stats) {
760         ByteArrayOutputStream baos = new ByteArrayOutputStream();
761         DataOutputStream out = new DataOutputStream(baos);
762         try {
763             out.writeLong(stats.beginTime);
764             UsageStatsXml.write(out, stats);
765         } catch (IOException ioe) {
766             Slog.d(TAG, "Serializing IntervalStats Failed", ioe);
767             baos.reset();
768         }
769         return baos.toByteArray();
770     }
771 
deserializeIntervalStats(byte[] data)772     private static IntervalStats deserializeIntervalStats(byte[] data) {
773         ByteArrayInputStream bais = new ByteArrayInputStream(data);
774         DataInputStream in = new DataInputStream(bais);
775         IntervalStats stats = new IntervalStats();
776         try {
777             stats.beginTime = in.readLong();
778             UsageStatsXml.read(in, stats);
779         } catch (IOException ioe) {
780             Slog.d(TAG, "DeSerializing IntervalStats Failed", ioe);
781             stats = null;
782         }
783         return stats;
784     }
785 
deleteDirectoryContents(File directory)786     private static void deleteDirectoryContents(File directory) {
787         File[] files = directory.listFiles();
788         for (File file : files) {
789             deleteDirectory(file);
790         }
791     }
792 
deleteDirectory(File directory)793     private static void deleteDirectory(File directory) {
794         File[] files = directory.listFiles();
795         if (files != null) {
796             for (File file : files) {
797                 if (!file.isDirectory()) {
798                     file.delete();
799                 } else {
800                     deleteDirectory(file);
801                 }
802             }
803         }
804         directory.delete();
805     }
806 }