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