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
17package com.android.server.usage;
18
19import android.app.usage.TimeSparseArray;
20import android.app.usage.UsageStats;
21import android.app.usage.UsageStatsManager;
22import android.os.Build;
23import android.os.SystemProperties;
24import android.util.AtomicFile;
25import android.util.Slog;
26import android.util.TimeUtils;
27
28import java.io.BufferedReader;
29import java.io.BufferedWriter;
30import java.io.ByteArrayInputStream;
31import java.io.ByteArrayOutputStream;
32import java.io.DataInputStream;
33import java.io.DataOutputStream;
34import java.io.File;
35import java.io.FileReader;
36import java.io.FileWriter;
37import java.io.FilenameFilter;
38import java.io.IOException;
39import java.util.ArrayList;
40import java.util.List;
41
42/**
43 * Provides an interface to query for UsageStat data from an XML database.
44 */
45class 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
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     */
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 {
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     */
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
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     */
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     */
232    boolean isNewUpdate() {
233        return mNewUpdate;
234    }
235
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
274    private String getBuildFingerprint() {
275        return Build.VERSION.RELEASE + ";"
276                + Build.VERSION.CODENAME + ";"
277                + Build.VERSION.INCREMENTAL;
278    }
279
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
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     */
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         */
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     */
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     */
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     */
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
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
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     */
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 */
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
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     */
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
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
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
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
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
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
786    private static void deleteDirectoryContents(File directory) {
787        File[] files = directory.listFiles();
788        for (File file : files) {
789            deleteDirectory(file);
790        }
791    }
792
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}