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

Commit 2c0b9f5d authored by Varun Shah's avatar Varun Shah
Browse files

Prune UsageStats data belonging to uninstalled packages.

Add a per-user job to prune any usage stats data which belongs to a
recently removed package. This job will be scheduled when a package is
removed and it will be executed when the device is considered to be
idle. The jobs are persisted by job scheduler and they are also removed
when a user is removed since all usage stats data is deleted on user
removal.

When executed, the job reads all of the stats on disk and removes those
that belong to removed packages (packages which don't have a token
mapping). If no data is ommitted on read, a write is not performed. If
the user is in a locked state when the job is executed, the job will
keep getting rescheduled until a sucessful pruning of the data.

Additionally, add logic to prune any obsolete usage stats data on a
database upgrade, pruning all data belonging to packages which have
been uninstalled. This ensures that all data in UsageStats in R belongs
to packages that are currently installed or to packages whose
DONT_DELETE_DATA flag was set when uninstalling.

Also remove the clean-up mappings step on boot. That was added as a
safety measure to ensure the mappings file is always updated. However,
with the logic to prune on upgrade and on package uninstalls, that step
is now unnecessary.

This CL also refactors how the UserUsageStatsService is fetched and
initialized within UsageStatsService. This is to ensure there is no lock
contention when the user service is initialized and it also makes way
for other refactorings in the future related to the user service
initialization.

Bug: 143889121
Test: atest UsageStatsDatabaseTest
Test: atest android.app.usage.cts.UsageStatsTest
Change-Id: If475fc018a930d0956b85a64b4e34e2c75b2476f
parent da0af120
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -5045,6 +5045,10 @@
                 android:permission="android.permission.BIND_JOB_SERVICE" >
        </service>

        <service android:name="com.android.server.usage.UsageStatsIdleService"
                 android:permission="android.permission.BIND_JOB_SERVICE" >
        </service>

        <service android:name="com.android.server.net.watchlist.ReportWatchlistJobService"
                 android:permission="android.permission.BIND_JOB_SERVICE" >
        </service>
+9 −0
Original line number Diff line number Diff line
@@ -281,4 +281,13 @@ public abstract class UsageStatsManagerInternal {
            return mUsageRemaining;
        }
    }

    /**
     * Called by {@link com.android.server.usage.UsageStatsIdleService} when the device is idle to
     * prune usage stats data for uninstalled packages.
     *
     * @param userId the user associated with the job
     * @return {@code true} if the pruning was successful, {@code false} otherwise
     */
    public abstract boolean pruneUninstalledPackagesData(@UserIdInt int userId);
}
+101 −11
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Set;

@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -93,6 +94,8 @@ public class UsageStatsDatabaseTest {
                for (File f : usageFiles) {
                    f.delete();
                }
            } else {
                intervalDir.delete();
            }
        }
    }
@@ -587,6 +590,7 @@ public class UsageStatsDatabaseTest {
        db.readMappingsLocked();
        db.init(1);
        db.putUsageStats(interval, mIntervalStats);
        db.writeMappingsLocked();

        final String removedPackage = "fake.package.name0";
        // invoke handler call directly from test to remove package
@@ -594,21 +598,21 @@ public class UsageStatsDatabaseTest {

        List<IntervalStats> stats = db.queryUsageStats(interval, 0, mEndTime,
                mIntervalStatsVerifier);
        for (int i = 0; i < stats.size(); i++) {
            final IntervalStats stat = stats.get(i);
        assertEquals(1, stats.size(),
                "Only one interval stats object should exist for the given time range.");
        final IntervalStats stat = stats.get(0);
        if (stat.packageStats.containsKey(removedPackage)) {
            fail("Found removed package " + removedPackage + " in package stats.");
            return;
        }
            for (int j = 0; j < stat.events.size(); j++) {
                final Event event = stat.events.get(j);
        for (int i = 0; i < stat.events.size(); i++) {
            final Event event = stat.events.get(i);
            if (removedPackage.equals(event.mPackage)) {
                fail("Found an event from removed package " + removedPackage);
                return;
            }
        }
    }
    }

    @Test
    public void testPackageRetention() throws IOException {
@@ -617,4 +621,90 @@ public class UsageStatsDatabaseTest {
        verifyPackageNotRetained(UsageStatsManager.INTERVAL_MONTHLY);
        verifyPackageNotRetained(UsageStatsManager.INTERVAL_YEARLY);
    }

    private void verifyPackageDataIsRemoved(UsageStatsDatabase db, int interval,
            String removedPackage) {
        List<IntervalStats> stats = db.queryUsageStats(interval, 0, mEndTime,
                mIntervalStatsVerifier);
        assertEquals(1, stats.size(),
                "Only one interval stats object should exist for the given time range.");
        final IntervalStats stat = stats.get(0);
        if (stat.packageStats.containsKey(removedPackage)) {
            fail("Found removed package " + removedPackage + " in package stats.");
            return;
        }
        for (int i = 0; i < stat.events.size(); i++) {
            final Event event = stat.events.get(i);
            if (removedPackage.equals(event.mPackage)) {
                fail("Found an event from removed package " + removedPackage);
                return;
            }
        }
    }

    private void verifyPackageDataIsNotRemoved(UsageStatsDatabase db, int interval,
            Set<String> installedPackages) {
        List<IntervalStats> stats = db.queryUsageStats(interval, 0, mEndTime,
                mIntervalStatsVerifier);
        assertEquals(1, stats.size(),
                "Only one interval stats object should exist for the given time range.");
        final IntervalStats stat = stats.get(0);
        if (!stat.packageStats.containsAll(installedPackages)) {
            fail("Could not find some installed packages in package stats.");
            return;
        }
        // attempt to find an event from each installed package
        for (String installedPackage : installedPackages) {
            for (int i = 0; i < stat.events.size(); i++) {
                if (installedPackage.equals(stat.events.get(i).mPackage)) {
                    break;
                }
                if (i == stat.events.size() - 1) {
                    fail("Could not find any event for: " + installedPackage);
                    return;
                }
            }
        }
    }

    @Test
    public void testPackageDataIsRemoved() throws IOException {
        UsageStatsDatabase db = new UsageStatsDatabase(mTestDir);
        db.readMappingsLocked();
        db.init(1);

        // write stats to disk for each interval
        db.putUsageStats(UsageStatsManager.INTERVAL_DAILY, mIntervalStats);
        db.putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, mIntervalStats);
        db.putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, mIntervalStats);
        db.putUsageStats(UsageStatsManager.INTERVAL_YEARLY, mIntervalStats);
        db.writeMappingsLocked();

        final Set<String> installedPackages = mIntervalStats.packageStats.keySet();
        final String removedPackage = installedPackages.iterator().next();
        installedPackages.remove(removedPackage);

        // mimic a package uninstall
        db.onPackageRemoved(removedPackage, System.currentTimeMillis());

        // mimic the idle prune job being triggered
        db.pruneUninstalledPackagesData();

        // read data from disk into a new db instance
        UsageStatsDatabase newDB = new UsageStatsDatabase(mTestDir);
        newDB.readMappingsLocked();
        newDB.init(mEndTime);

        // query data for each interval and ensure data for package doesn't exist
        verifyPackageDataIsRemoved(newDB, UsageStatsManager.INTERVAL_DAILY, removedPackage);
        verifyPackageDataIsRemoved(newDB, UsageStatsManager.INTERVAL_WEEKLY, removedPackage);
        verifyPackageDataIsRemoved(newDB, UsageStatsManager.INTERVAL_MONTHLY, removedPackage);
        verifyPackageDataIsRemoved(newDB, UsageStatsManager.INTERVAL_YEARLY, removedPackage);

        // query data for each interval and ensure some data for installed packages exists
        verifyPackageDataIsNotRemoved(newDB, UsageStatsManager.INTERVAL_DAILY, installedPackages);
        verifyPackageDataIsNotRemoved(newDB, UsageStatsManager.INTERVAL_WEEKLY, installedPackages);
        verifyPackageDataIsNotRemoved(newDB, UsageStatsManager.INTERVAL_MONTHLY, installedPackages);
        verifyPackageDataIsNotRemoved(newDB, UsageStatsManager.INTERVAL_YEARLY, installedPackages);
    }
}
+19 −5
Original line number Diff line number Diff line
@@ -448,8 +448,11 @@ public class IntervalStats {
    /**
     * Parses all of the tokens to strings in the obfuscated usage stats data. This includes
     * deobfuscating each of the package tokens and chooser actions and categories.
     *
     * @return {@code true} if any stats were omitted while deobfuscating, {@code false} otherwise.
     */
    private void deobfuscateUsageStats(PackagesTokenData packagesTokenData) {
    private boolean deobfuscateUsageStats(PackagesTokenData packagesTokenData) {
        boolean dataOmitted = false;
        final int usageStatsSize = packageStatsObfuscated.size();
        for (int statsIndex = 0; statsIndex < usageStatsSize; statsIndex++) {
            final int packageToken = packageStatsObfuscated.keyAt(statsIndex);
@@ -457,6 +460,7 @@ public class IntervalStats {
            usageStats.mPackageName = packagesTokenData.getPackageString(packageToken);
            if (usageStats.mPackageName == null) {
                Slog.e(TAG, "Unable to parse usage stats package " + packageToken);
                dataOmitted = true;
                continue;
            }

@@ -489,14 +493,18 @@ public class IntervalStats {
            }
            packageStats.put(usageStats.mPackageName, usageStats);
        }
        return dataOmitted;
    }

    /**
     * Parses all of the tokens to strings in the obfuscated events data. This includes
     * deobfuscating the package token, along with any class, task root package/class tokens, and
     * shortcut or notification channel tokens.
     *
     * @return {@code true} if any events were omitted while deobfuscating, {@code false} otherwise.
     */
    private void deobfuscateEvents(PackagesTokenData packagesTokenData) {
    private boolean deobfuscateEvents(PackagesTokenData packagesTokenData) {
        boolean dataOmitted = false;
        for (int i = this.events.size() - 1; i >= 0; i--) {
            final Event event = this.events.get(i);
            final int packageToken = event.mPackageToken;
@@ -504,6 +512,7 @@ public class IntervalStats {
            if (event.mPackage == null) {
                Slog.e(TAG, "Unable to parse event package " + packageToken);
                this.events.remove(i);
                dataOmitted = true;
                continue;
            }

@@ -543,6 +552,7 @@ public class IntervalStats {
                        Slog.e(TAG, "Unable to parse shortcut " + event.mShortcutIdToken
                                + " for package " + packageToken);
                        this.events.remove(i);
                        dataOmitted = true;
                        continue;
                    }
                    break;
@@ -554,21 +564,25 @@ public class IntervalStats {
                                + event.mNotificationChannelIdToken + " for package "
                                + packageToken);
                        this.events.remove(i);
                        dataOmitted = true;
                        continue;
                    }
                    break;
            }
        }
        return dataOmitted;
    }

    /**
     * Parses the obfuscated tokenized data held in this interval stats object.
     *
     * @return {@code true} if any data was omitted while deobfuscating, {@code false} otherwise.
     * @hide
     */
    public void deobfuscateData(PackagesTokenData packagesTokenData) {
        deobfuscateUsageStats(packagesTokenData);
        deobfuscateEvents(packagesTokenData);
    public boolean deobfuscateData(PackagesTokenData packagesTokenData) {
        final boolean statsOmitted = deobfuscateUsageStats(packagesTokenData);
        final boolean eventsOmitted = deobfuscateEvents(packagesTokenData);
        return statsOmitted || eventsOmitted;
    }

    /**
+5 −2
Original line number Diff line number Diff line
@@ -162,15 +162,18 @@ public final class PackagesTokenData {
     *
     * @param packageName the package to be removed
     * @param timeRemoved the time stamp of when the package was removed
     * @return the token mapped to the package removed or {@code PackagesTokenData.UNASSIGNED_TOKEN}
     *         if not mapped
     */
    public void removePackage(String packageName, long timeRemoved) {
    public int removePackage(String packageName, long timeRemoved) {
        removedPackagesMap.put(packageName, timeRemoved);

        if (!packagesToTokensMap.containsKey(packageName)) {
            return;
            return UNASSIGNED_TOKEN;
        }
        final int packageToken = packagesToTokensMap.get(packageName).get(packageName);
        packagesToTokensMap.remove(packageName);
        tokensToPackagesMap.delete(packageToken);
        return packageToken;
    }
}
Loading