Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cd922b29 authored by Adam Lesinski's avatar Adam Lesinski Committed by Android (Google) Code Review
Browse files

Merge "Returns UsageEvents from previous days" into lmp-dev

parents b5928445 d26bea3a
Loading
Loading
Loading
Loading
+99 −0
Original line number Diff line number Diff line
@@ -13,51 +13,87 @@
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package com.android.server.usage;

import android.app.usage.UsageStatsManager;

import java.util.Calendar;

/**
 * A collection of utility methods used by the UsageStatsService and accompanying classes.
 * A handy calendar object that knows nothing of Locale's or TimeZones. This simplifies
 * interval book-keeping. It is *NOT* meant to be used as a user-facing calendar, as it has
 * no concept of Locale or TimeZone.
 */
final class UsageStatsUtils {
    private UsageStatsUtils() {}
public class UnixCalendar {
    private static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
    private static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
    private static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
    private static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
    private long mTime;

    /**
     * Truncates the date to the given UsageStats bucket. For example, if the bucket is
     * {@link UsageStatsManager#INTERVAL_YEARLY}, the date is truncated to the 1st day of the year,
     * with the time set to 00:00:00.
     *
     * @param bucket The UsageStats bucket to truncate to.
     * @param cal The date to truncate.
     */
    public static void truncateDateTo(int bucket, Calendar cal) {
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
    public UnixCalendar(long time) {
        mTime = time;
    }

    public void truncateToDay() {
        mTime -= mTime % DAY_IN_MILLIS;
    }

    public void truncateToWeek() {
        mTime -= mTime % WEEK_IN_MILLIS;
    }

    public void truncateToMonth() {
        mTime -= mTime % MONTH_IN_MILLIS;
    }

    public void truncateToYear() {
        mTime -= mTime % YEAR_IN_MILLIS;
    }

    public void addDays(int val) {
        mTime += val * DAY_IN_MILLIS;
    }

    public void addWeeks(int val) {
        mTime += val * WEEK_IN_MILLIS;
    }

    public void addMonths(int val) {
        mTime += val * MONTH_IN_MILLIS;
    }

    public void addYears(int val) {
        mTime += val * YEAR_IN_MILLIS;
    }

    public void setTimeInMillis(long time) {
        mTime = time;
    }

    public long getTimeInMillis() {
        return mTime;
    }

        switch (bucket) {
    public static void truncateTo(UnixCalendar calendar, int intervalType) {
        switch (intervalType) {
            case UsageStatsManager.INTERVAL_YEARLY:
                cal.set(Calendar.DAY_OF_YEAR, 0);
                calendar.truncateToYear();
                break;

            case UsageStatsManager.INTERVAL_MONTHLY:
                cal.set(Calendar.DAY_OF_MONTH, 0);
                calendar.truncateToMonth();
                break;

            case UsageStatsManager.INTERVAL_WEEKLY:
                cal.set(Calendar.DAY_OF_WEEK, 0);
                calendar.truncateToWeek();
                break;

            case UsageStatsManager.INTERVAL_DAILY:
                calendar.truncateToDay();
                break;

            default:
                throw new UnsupportedOperationException("Can't truncate date to bucket " + bucket);
                throw new UnsupportedOperationException("Can't truncate date to interval " +
                        intervalType);
        }
    }
}
+82 −13
Original line number Diff line number Diff line
@@ -21,24 +21,30 @@ import android.app.usage.UsageStatsManager;
import android.util.AtomicFile;
import android.util.Slog;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * Provides an interface to query for UsageStat data from an XML database.
 */
class UsageStatsDatabase {
    private static final int CURRENT_VERSION = 1;

    private static final String TAG = "UsageStatsDatabase";
    private static final boolean DEBUG = UsageStatsService.DEBUG;

    private final Object mLock = new Object();
    private final File[] mIntervalDirs;
    private final TimeSparseArray<AtomicFile>[] mSortedStatFiles;
    private final Calendar mCal;
    private final UnixCalendar mCal;
    private final File mVersionFile;

    public UsageStatsDatabase(File dir) {
        mIntervalDirs = new File[] {
@@ -47,8 +53,9 @@ class UsageStatsDatabase {
                new File(dir, "monthly"),
                new File(dir, "yearly"),
        };
        mVersionFile = new File(dir, "version");
        mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
        mCal = Calendar.getInstance();
        mCal = new UnixCalendar(0);
    }

    /**
@@ -64,6 +71,8 @@ class UsageStatsDatabase {
                }
            }

            checkVersionLocked();

            final FilenameFilter backupFileFilter = new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
@@ -88,6 +97,43 @@ class UsageStatsDatabase {
        }
    }

    private void checkVersionLocked() {
        int version;
        try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
            version = Integer.parseInt(reader.readLine());
        } catch (NumberFormatException | IOException e) {
            version = 0;
        }

        if (version != CURRENT_VERSION) {
            Slog.i(TAG, "Upgrading from version " + version + " to " + CURRENT_VERSION);
            doUpgradeLocked(version, CURRENT_VERSION);

            try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
                writer.write(Integer.toString(CURRENT_VERSION));
            } catch (IOException e) {
                Slog.e(TAG, "Failed to write new version");
                throw new RuntimeException(e);
            }
        }
    }

    private void doUpgradeLocked(int thisVersion, int nextVersion) {
        if (thisVersion == 0) {
            // Delete all files if we are version 0. This is a pre-release version,
            // so this is fine.
            Slog.i(TAG, "Deleting all usage stats files");
            for (int i = 0; i < mIntervalDirs.length; i++) {
                File[] files = mIntervalDirs[i].listFiles();
                if (files != null) {
                    for (File f : files) {
                        f.delete();
                    }
                }
            }
        }
    }

    /**
     * Get the latest stats that exist for this interval type.
     */
@@ -161,25 +207,48 @@ class UsageStatsDatabase {
                throw new IllegalArgumentException("Bad interval type " + intervalType);
            }

            if (endTime < beginTime) {
            final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];

            if (endTime <= beginTime) {
                if (DEBUG) {
                    Slog.d(TAG, "endTime(" + endTime + ") <= beginTime(" + beginTime + ")");
                }
                return null;
            }

            final int startIndex = mSortedStatFiles[intervalType].closestIndexOnOrBefore(beginTime);
            int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
            if (startIndex < 0) {
                // All the stats available have timestamps after beginTime, which means they all
                // match.
                startIndex = 0;
            }

            int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
            if (endIndex < 0) {
                // All the stats start after this range ends, so nothing matches.
                if (DEBUG) {
                    Slog.d(TAG, "No results for this range. All stats start after.");
                }
                return null;
            }

            int endIndex = mSortedStatFiles[intervalType].closestIndexOnOrAfter(endTime);
            if (intervalStats.keyAt(endIndex) == endTime) {
                // The endTime is exclusive, so if we matched exactly take the one before.
                endIndex--;
                if (endIndex < 0) {
                endIndex = mSortedStatFiles[intervalType].size() - 1;
                    // All the stats start after this range ends, so nothing matches.
                    if (DEBUG) {
                        Slog.d(TAG, "No results for this range. All stats start after.");
                    }
                    return null;
                }
            }

            try {
                IntervalStats stats = new IntervalStats();
                ArrayList<T> results = new ArrayList<>();
                for (int i = startIndex; i <= endIndex; i++) {
                    final AtomicFile f = mSortedStatFiles[intervalType].valueAt(i);
                    final AtomicFile f = intervalStats.valueAt(i);

                    if (DEBUG) {
                        Slog.d(TAG, "Reading stat file " + f.getBaseFile().getAbsolutePath());
@@ -230,22 +299,22 @@ class UsageStatsDatabase {
        synchronized (mLock) {
            long timeNow = System.currentTimeMillis();
            mCal.setTimeInMillis(timeNow);
            mCal.add(Calendar.YEAR, -3);
            mCal.addYears(-3);
            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_YEARLY],
                    mCal.getTimeInMillis());

            mCal.setTimeInMillis(timeNow);
            mCal.add(Calendar.MONTH, -6);
            mCal.addMonths(-6);
            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_MONTHLY],
                    mCal.getTimeInMillis());

            mCal.setTimeInMillis(timeNow);
            mCal.add(Calendar.WEEK_OF_YEAR, -4);
            mCal.addWeeks(-4);
            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_WEEKLY],
                    mCal.getTimeInMillis());

            mCal.setTimeInMillis(timeNow);
            mCal.add(Calendar.DAY_OF_YEAR, -7);
            mCal.addDays(-7);
            pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
                    mCal.getTimeInMillis());
        }
+1 −3
Original line number Diff line number Diff line
@@ -62,9 +62,7 @@ public class UsageStatsService extends SystemService implements
    static final boolean DEBUG = false;
    private static final long TEN_SECONDS = 10 * 1000;
    private static final long TWENTY_MINUTES = 20 * 60 * 1000;
    private static final long TWO_MINUTES = 2 * 60 * 1000;
    private static final long FLUSH_INTERVAL = DEBUG ? TEN_SECONDS : TWENTY_MINUTES;
    private static final long END_TIME_DELAY = DEBUG ? 0 : TWO_MINUTES;

    // Handler message types.
    static final int MSG_REPORT_EVENT = 0;
@@ -178,7 +176,7 @@ public class UsageStatsService extends SystemService implements
    }

    /**
     * Called by the Bunder stub
     * Called by the Binder stub
     */
    void shutdown() {
        synchronized (mLock) {
+119 −94
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;

/**
@@ -47,7 +46,7 @@ class UserUsageStatsService {
    private final UsageStatsDatabase mDatabase;
    private final IntervalStats[] mCurrentStats;
    private boolean mStatsChanged = false;
    private final Calendar mDailyExpiryDate;
    private final UnixCalendar mDailyExpiryDate;
    private final StatsUpdatedListener mListener;
    private final String mLogPrefix;

@@ -56,7 +55,7 @@ class UserUsageStatsService {
    }

    UserUsageStatsService(int userId, File usageStatsDir, StatsUpdatedListener listener) {
        mDailyExpiryDate = Calendar.getInstance();
        mDailyExpiryDate = new UnixCalendar(0);
        mDatabase = new UsageStatsDatabase(usageStatsDir);
        mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
        mListener = listener;
@@ -66,6 +65,7 @@ class UserUsageStatsService {
    void init() {
        mDatabase.init();

        final long timeNow = System.currentTimeMillis();
        int nullCount = 0;
        for (int i = 0; i < mCurrentStats.length; i++) {
            mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
@@ -73,6 +73,11 @@ class UserUsageStatsService {
                // Find out how many intervals we don't have data for.
                // Ideally it should be all or none.
                nullCount++;
            } else if (mCurrentStats[i].beginTime > timeNow) {
                Slog.e(TAG, mLogPrefix + "Interval " + i + " has stat in the future " +
                        mCurrentStats[i].beginTime);
                mCurrentStats[i] = null;
                nullCount++;
            }
        }

@@ -94,10 +99,11 @@ class UserUsageStatsService {
            // that is reported.
            mDailyExpiryDate.setTimeInMillis(
                    mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
            mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
            UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
            Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
                    + sDateFormat.format(mDailyExpiryDate.getTime()));
            mDailyExpiryDate.addDays(1);
            mDailyExpiryDate.truncateToDay();
            Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
                    sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) +
                    "(" + mDailyExpiryDate.getTimeInMillis() + ")");
        }

        // Now close off any events that were open at the time this was saved.
@@ -195,49 +201,68 @@ class UserUsageStatsService {
     * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
     * provided to select the stats to use from the IntervalStats object.
     */
    private <T> List<T> queryStats(int bucketType, long beginTime, long endTime,
    private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
            StatCombiner<T> combiner) {
        if (bucketType == UsageStatsManager.INTERVAL_BEST) {
            bucketType = mDatabase.findBestFitBucket(beginTime, endTime);
        if (intervalType == UsageStatsManager.INTERVAL_BEST) {
            intervalType = mDatabase.findBestFitBucket(beginTime, endTime);
            if (intervalType < 0) {
                // Nothing saved to disk yet, so every stat is just as equal (no rollover has
                // occurred.
                intervalType = UsageStatsManager.INTERVAL_DAILY;
            }
        }

        if (bucketType < 0 || bucketType >= mCurrentStats.length) {
        if (intervalType < 0 || intervalType >= mCurrentStats.length) {
            if (DEBUG) {
                Slog.d(TAG, mLogPrefix + "Bad bucketType used " + bucketType);
                Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType);
            }
            return null;
        }

        if (beginTime >= mCurrentStats[bucketType].endTime) {
        final IntervalStats currentStats = mCurrentStats[intervalType];

        if (DEBUG) {
            Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= "
                    + beginTime + " AND endTime < " + endTime);
        }

        if (beginTime >= currentStats.endTime) {
            if (DEBUG) {
                Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
                        + mCurrentStats[bucketType].endTime);
                        + currentStats.endTime);
            }
            // Nothing newer available.
            return null;
        }

        // Truncate the endTime to just before the in-memory stats. Then, we'll append the
        // in-memory stats to the results (if necessary) so as to avoid writing to disk too
        // often.
        final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);

        } else if (beginTime >= mCurrentStats[bucketType].beginTime) {
        // Get the stats from disk.
        List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,
                truncatedEndTime, combiner);
        if (DEBUG) {
                Slog.d(TAG, mLogPrefix + "Returning in-memory stats for bucket " + bucketType);
            }
            // Fast path for retrieving in-memory state.
            ArrayList<T> results = new ArrayList<>();
            combiner.combine(mCurrentStats[bucketType], true, results);
            return results;
            Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk");
            Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime +
                    " endTime=" + currentStats.endTime);
        }

        // Flush any changes that were made to disk before we do a disk query.
        // If we're not grabbing the ongoing stats, no need to persist.
        persistActiveStats();

        // Now check if the in-memory stats match the range and add them if they do.
        if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
            if (DEBUG) {
            Slog.d(TAG, mLogPrefix + "SELECT * FROM " + bucketType + " WHERE beginTime >= "
                    + beginTime + " AND endTime < " + endTime);
                Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
            }

            if (results == null) {
                results = new ArrayList<>();
            }
            combiner.combine(currentStats, true, results);
        }

        final List<T> results = mDatabase.queryUsageStats(bucketType, beginTime, endTime, combiner);
        if (DEBUG) {
            Slog.d(TAG, mLogPrefix + "Results: " + (results == null ? 0 : results.size()));
            Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0));
        }
        return results;
    }
@@ -250,46 +275,47 @@ class UserUsageStatsService {
        return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
    }

    UsageEvents queryEvents(long beginTime, long endTime) {
        if (endTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime) {
            if (beginTime > mCurrentStats[UsageStatsManager.INTERVAL_DAILY].endTime) {
                return null;
            }

            TimeSparseArray<UsageEvents.Event> events =
                    mCurrentStats[UsageStatsManager.INTERVAL_DAILY].events;
            if (events == null) {
                return null;
    UsageEvents queryEvents(final long beginTime, final long endTime) {
        final ArraySet<String> names = new ArraySet<>();
        List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
                beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
                    @Override
                    public void combine(IntervalStats stats, boolean mutable,
                            List<UsageEvents.Event> accumulatedResult) {
                        if (stats.events == null) {
                            return;
                        }

            final int startIndex = events.closestIndexOnOrAfter(beginTime);
                        final int startIndex = stats.events.closestIndexOnOrAfter(beginTime);
                        if (startIndex < 0) {
                return null;
                            return;
                        }

            ArraySet<String> names = new ArraySet<>();
            ArrayList<UsageEvents.Event> results = new ArrayList<>();
            final int size = events.size();
                        final int size = stats.events.size();
                        for (int i = startIndex; i < size; i++) {
                if (events.keyAt(i) >= endTime) {
                    break;
                            if (stats.events.keyAt(i) >= endTime) {
                                return;
                            }
                final UsageEvents.Event event = events.valueAt(i);

                            final UsageEvents.Event event = stats.events.valueAt(i);
                            names.add(event.mPackage);
                            if (event.mClass != null) {
                                names.add(event.mClass);
                            }
                results.add(event);
                            accumulatedResult.add(event);
                        }
            String[] table = names.toArray(new String[names.size()]);
            Arrays.sort(table);
            return new UsageEvents(results, table);
                    }
                });

        // TODO(adamlesinski): Query the previous days.
        if (results == null || results.isEmpty()) {
            return null;
        }

        String[] table = names.toArray(new String[names.size()]);
        Arrays.sort(table);
        return new UsageEvents(results, table);
    }

    void persistActiveStats() {
        if (mStatsChanged) {
            Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
@@ -360,54 +386,53 @@ class UserUsageStatsService {
    private void loadActiveStats() {
        final long timeNow = System.currentTimeMillis();

        Calendar tempCal = mDailyExpiryDate;
        for (int bucketType = 0; bucketType < mCurrentStats.length; bucketType++) {
        final UnixCalendar tempCal = mDailyExpiryDate;
        for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
            tempCal.setTimeInMillis(timeNow);
            UsageStatsUtils.truncateDateTo(bucketType, tempCal);
            UnixCalendar.truncateTo(tempCal, intervalType);

            if (mCurrentStats[bucketType] != null &&
                    mCurrentStats[bucketType].beginTime == tempCal.getTimeInMillis()) {
            if (mCurrentStats[intervalType] != null &&
                    mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) {
                // These are the same, no need to load them (in memory stats are always newer
                // than persisted stats).
                continue;
            }

            final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(bucketType);
            if (lastBeginTime >= tempCal.getTimeInMillis()) {
            final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType);
            if (lastBeginTime > timeNow) {
                Slog.e(TAG, mLogPrefix + "Latest usage stats for interval " +
                        intervalType + " begins in the future");
                mCurrentStats[intervalType] = null;
            } else if (lastBeginTime >= tempCal.getTimeInMillis()) {
                if (DEBUG) {
                    Slog.d(TAG, mLogPrefix + "Loading existing stats (" + lastBeginTime +
                            ") for bucket " + bucketType);
                }
                mCurrentStats[bucketType] = mDatabase.getLatestUsageStats(bucketType);
                if (DEBUG) {
                    if (mCurrentStats[bucketType] != null) {
                        Slog.d(TAG, mLogPrefix + "Found " +
                                (mCurrentStats[bucketType].events == null ?
                                        0 : mCurrentStats[bucketType].events.size()) +
                                " events");
                    }
                    Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
                            sDateFormat.format(lastBeginTime) + "(" + lastBeginTime +
                            ") for interval " + intervalType);
                }
                mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType);
            } else {
                mCurrentStats[bucketType] = null;
                mCurrentStats[intervalType] = null;
            }

            if (mCurrentStats[bucketType] == null) {
            if (mCurrentStats[intervalType] == null) {
                if (DEBUG) {
                    Slog.d(TAG, "Creating new stats (" + tempCal.getTimeInMillis() +
                            ") for bucket " + bucketType);
                    Slog.d(TAG, "Creating new stats @ " +
                            sDateFormat.format(tempCal.getTimeInMillis()) + "(" +
                            tempCal.getTimeInMillis() + ") for interval " + intervalType);

                }
                mCurrentStats[bucketType] = new IntervalStats();
                mCurrentStats[bucketType].beginTime = tempCal.getTimeInMillis();
                mCurrentStats[bucketType].endTime = timeNow;
                mCurrentStats[intervalType] = new IntervalStats();
                mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis();
                mCurrentStats[intervalType].endTime = timeNow;
            }
        }
        mStatsChanged = false;
        mDailyExpiryDate.setTimeInMillis(timeNow);
        mDailyExpiryDate.add(Calendar.DAY_OF_YEAR, 1);
        UsageStatsUtils.truncateDateTo(UsageStatsManager.INTERVAL_DAILY, mDailyExpiryDate);
        Slog.i(TAG, mLogPrefix + "Rollover scheduled for "
                + sDateFormat.format(mDailyExpiryDate.getTime()));
        mDailyExpiryDate.addDays(1);
        mDailyExpiryDate.truncateToDay();
        Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
                sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" +
                tempCal.getTimeInMillis() + ")");
    }