diff options
9 files changed, 511 insertions, 102 deletions
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 9dc23199ffae..f6e91efd2ad7 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5070,6 +5070,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> diff --git a/services/core/java/android/app/usage/UsageStatsManagerInternal.java b/services/core/java/android/app/usage/UsageStatsManagerInternal.java index 6641b5be651d..2f8c506d5ea7 100644 --- a/services/core/java/android/app/usage/UsageStatsManagerInternal.java +++ b/services/core/java/android/app/usage/UsageStatsManagerInternal.java @@ -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); } diff --git a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java index e32103fe6bff..e6bb244ef05b 100644 --- a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java +++ b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java @@ -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,19 +598,19 @@ 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); - if (stat.packageStats.containsKey(removedPackage)) { - fail("Found removed package " + removedPackage + " in package stats."); + 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; } - for (int j = 0; j < stat.events.size(); j++) { - final Event event = stat.events.get(j); - if (removedPackage.equals(event.mPackage)) { - fail("Found an event from removed package " + removedPackage); - return; - } - } } } @@ -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); + } } diff --git a/services/usage/java/com/android/server/usage/IntervalStats.java b/services/usage/java/com/android/server/usage/IntervalStats.java index 46b261b64192..8fb283adc740 100644 --- a/services/usage/java/com/android/server/usage/IntervalStats.java +++ b/services/usage/java/com/android/server/usage/IntervalStats.java @@ -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; } /** diff --git a/services/usage/java/com/android/server/usage/PackagesTokenData.java b/services/usage/java/com/android/server/usage/PackagesTokenData.java index 4bf08a49af0f..f19abbbac485 100644 --- a/services/usage/java/com/android/server/usage/PackagesTokenData.java +++ b/services/usage/java/com/android/server/usage/PackagesTokenData.java @@ -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; } } diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java index 27d7360313ad..e34824c57fb2 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java +++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java @@ -29,6 +29,7 @@ import android.util.SparseArray; import android.util.TimeUtils; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.IndentingPrintWriter; import libcore.io.IoUtils; @@ -52,6 +53,7 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; /** @@ -122,6 +124,7 @@ public class UsageStatsDatabase { private int mCurrentVersion; private boolean mFirstUpdate; private boolean mNewUpdate; + private boolean mUpgradePerformed; // The obfuscated packages to tokens mappings file private final File mPackageMappingsFile; @@ -325,6 +328,13 @@ public class UsageStatsDatabase { return mNewUpdate; } + /** + * Was an upgrade performed when this database was initialized? + */ + boolean wasUpgradePerformed() { + return mUpgradePerformed; + } + private void checkVersionAndBuildLocked() { int version; String buildFingerprint; @@ -397,6 +407,8 @@ public class UsageStatsDatabase { if (mUpdateBreadcrumb.exists()) { // Files should be up to date with current version. Clear the version update breadcrumb mUpdateBreadcrumb.delete(); + // update mUpgradePerformed after breadcrumb is deleted to indicate a successful upgrade + mUpgradePerformed = true; } if (mBackupsDir.exists() && !KEEP_BACKUP_DIR) { @@ -545,12 +557,127 @@ public class UsageStatsDatabase { } } - void onPackageRemoved(String packageName, long timeRemoved) { + /** + * Returns the token mapped to the package removed or {@code PackagesTokenData.UNASSIGNED_TOKEN} + * if not mapped. + */ + int onPackageRemoved(String packageName, long timeRemoved) { + synchronized (mLock) { + final int tokenRemoved = mPackagesTokenData.removePackage(packageName, timeRemoved); + try { + writeMappingsLocked(); + } catch (Exception e) { + Slog.w(TAG, "Unable to update package mappings on disk after removing token " + + tokenRemoved); + } + return tokenRemoved; + } + } + + /** + * Reads all the usage stats data on disk and rewrites it with any data related to uninstalled + * packages omitted. Returns {@code true} on success, {@code false} otherwise. + */ + boolean pruneUninstalledPackagesData() { + synchronized (mLock) { + for (int i = 0; i < mIntervalDirs.length; i++) { + final File[] files = mIntervalDirs[i].listFiles(); + if (files == null) { + continue; + } + for (int j = 0; j < files.length; j++) { + try { + final IntervalStats stats = new IntervalStats(); + final AtomicFile atomicFile = new AtomicFile(files[j]); + if (!readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData)) { + continue; // no data was omitted when read so no need to rewrite + } + // Any data related to packages that have been removed would have failed + // the deobfuscation step on read so the IntervalStats object here only + // contains data for packages that are currently installed - all we need + // to do here is write the data back to disk. + writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData); + } catch (Exception e) { + Slog.e(TAG, "Failed to prune data from: " + files[j].toString()); + return false; + } + } + } + + try { + writeMappingsLocked(); + } catch (IOException e) { + Slog.e(TAG, "Failed to write package mappings after pruning data."); + return false; + } + return true; + } + } + + /** + * Iterates through all the files on disk and prunes any data that belongs to packages that have + * been uninstalled (packages that are not in the given list). + * Note: this should only be called once, when there has been a database upgrade. + * + * @param installedPackages map of installed packages (package_name:package_install_time) + */ + void prunePackagesDataOnUpgrade(HashMap<String, Long> installedPackages) { + if (ArrayUtils.isEmpty(installedPackages)) { + return; + } synchronized (mLock) { - mPackagesTokenData.removePackage(packageName, timeRemoved); + for (int i = 0; i < mIntervalDirs.length; i++) { + final File[] files = mIntervalDirs[i].listFiles(); + if (files == null) { + continue; + } + for (int j = 0; j < files.length; j++) { + try { + final IntervalStats stats = new IntervalStats(); + final AtomicFile atomicFile = new AtomicFile(files[j]); + readLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData); + if (!pruneStats(installedPackages, stats)) { + continue; // no stats were pruned so no need to rewrite + } + writeLocked(atomicFile, stats, mCurrentVersion, mPackagesTokenData); + } catch (Exception e) { + Slog.e(TAG, "Failed to prune data from: " + files[j].toString()); + } + } + } } } + private boolean pruneStats(HashMap<String, Long> installedPackages, IntervalStats stats) { + boolean dataPruned = false; + + // prune old package usage stats + for (int i = stats.packageStats.size() - 1; i >= 0; i--) { + final UsageStats usageStats = stats.packageStats.valueAt(i); + final Long timeInstalled = installedPackages.get(usageStats.mPackageName); + if (timeInstalled == null || timeInstalled > usageStats.mEndTimeStamp) { + stats.packageStats.removeAt(i); + dataPruned = true; + } + } + if (dataPruned) { + // ensure old stats don't linger around during the obfuscation step on write + stats.packageStatsObfuscated.clear(); + } + + // prune old events + for (int i = stats.events.size() - 1; i >= 0; i--) { + final UsageEvents.Event event = stats.events.get(i); + final Long timeInstalled = installedPackages.get(event.mPackage); + if (timeInstalled == null || timeInstalled > event.mTimeStamp) { + stats.events.remove(i); + dataPruned = true; + } + } + + return dataPruned; + } + public void onTimeChanged(long timeDiffMillis) { synchronized (mLock) { StringBuilder logBuilder = new StringBuilder(); @@ -645,7 +772,6 @@ public class UsageStatsDatabase { } // filter out events - final int eventsSize = stats.events.size(); for (int i = stats.events.size() - 1; i >= 0; i--) { final UsageEvents.Event event = stats.events.get(i); final Long timeRemoved = removedPackagesMap.get(event.mPackage); @@ -942,13 +1068,17 @@ public class UsageStatsDatabase { readLocked(file, statsOut, mCurrentVersion, mPackagesTokenData); } - private static void readLocked(AtomicFile file, IntervalStats statsOut, int version, + /** + * Returns {@code true} if any stats were omitted while reading, {@code false} otherwise. + */ + private static boolean readLocked(AtomicFile file, IntervalStats statsOut, int version, PackagesTokenData packagesTokenData) throws IOException { + boolean dataOmitted = false; try { FileInputStream in = file.openRead(); try { statsOut.beginTime = parseBeginTime(file); - readLocked(in, statsOut, version, packagesTokenData); + dataOmitted = readLocked(in, statsOut, version, packagesTokenData); statsOut.lastTimeSaved = file.getLastModifiedTime(); } finally { try { @@ -961,10 +1091,15 @@ public class UsageStatsDatabase { Slog.e(TAG, "UsageStatsDatabase", e); throw e; } + return dataOmitted; } - private static void readLocked(InputStream in, IntervalStats statsOut, int version, + /** + * Returns {@code true} if any stats were omitted while reading, {@code false} otherwise. + */ + private static boolean readLocked(InputStream in, IntervalStats statsOut, int version, PackagesTokenData packagesTokenData) throws IOException { + boolean dataOmitted = false; switch (version) { case 1: case 2: @@ -989,14 +1124,14 @@ public class UsageStatsDatabase { } catch (IOException e) { Slog.e(TAG, "Unable to read interval stats from proto.", e); } - statsOut.deobfuscateData(packagesTokenData); + dataOmitted = statsOut.deobfuscateData(packagesTokenData); break; default: throw new RuntimeException( "Unhandled UsageStatsDatabase version: " + Integer.toString(version) + " on read."); } - + return dataOmitted; } /** diff --git a/services/usage/java/com/android/server/usage/UsageStatsIdleService.java b/services/usage/java/com/android/server/usage/UsageStatsIdleService.java new file mode 100644 index 000000000000..4468871ee0fb --- /dev/null +++ b/services/usage/java/com/android/server/usage/UsageStatsIdleService.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.usage; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.os.AsyncTask; +import android.os.PersistableBundle; + +import com.android.server.LocalServices; + +/** + * JobService used to do any work for UsageStats while the device is idle. + */ +public class UsageStatsIdleService extends JobService { + + /** + * Base job ID for the pruning job - must be unique within the system server uid. + */ + private static final int PRUNE_JOB_ID = 546357475; + + private static final String USER_ID_KEY = "user_id"; + + static void scheduleJob(Context context, int userId) { + final int userJobId = PRUNE_JOB_ID + userId; // unique job id per user + final ComponentName component = new ComponentName(context.getPackageName(), + UsageStatsIdleService.class.getName()); + final PersistableBundle bundle = new PersistableBundle(); + bundle.putInt(USER_ID_KEY, userId); + final JobInfo pruneJob = new JobInfo.Builder(userJobId, component) + .setRequiresDeviceIdle(true) + .setExtras(bundle) + .setPersisted(true) + .build(); + + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + final JobInfo pendingPruneJob = jobScheduler.getPendingJob(userJobId); + // only schedule a new prune job if one doesn't exist already for this user + if (!pruneJob.equals(pendingPruneJob)) { + jobScheduler.cancel(userJobId); // cancel any previously scheduled prune job + jobScheduler.schedule(pruneJob); + } + + } + + static void cancelJob(Context context, int userId) { + final int userJobId = PRUNE_JOB_ID + userId; // unique job id per user + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + jobScheduler.cancel(userJobId); + } + + @Override + public boolean onStartJob(JobParameters params) { + final PersistableBundle bundle = params.getExtras(); + final int userId = bundle.getInt(USER_ID_KEY, -1); + if (userId == -1) { + return false; + } + + AsyncTask.execute(() -> { + final UsageStatsManagerInternal usageStatsManagerInternal = LocalServices.getService( + UsageStatsManagerInternal.class); + final boolean pruned = usageStatsManagerInternal.pruneUninstalledPackagesData(userId); + jobFinished(params, !pruned); // reschedule if data was not pruned + }); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + // Since the pruning job isn't a heavy job, we don't want to cancel it's execution midway. + return false; + } +} diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index ba4a4484f09b..064922386773 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -52,6 +52,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; @@ -99,6 +100,7 @@ import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -318,6 +320,8 @@ public class UsageStatsService extends SystemService implements } private void onUserUnlocked(int userId) { + // fetch the installed packages outside the lock so it doesn't block package manager. + final HashMap<String, Long> installedPackages = getInstalledPackages(userId); synchronized (mLock) { // Create a user unlocked event to report final Event unlockEvent = new Event(USER_UNLOCKED, SystemClock.elapsedRealtime()); @@ -334,9 +338,10 @@ public class UsageStatsService extends SystemService implements } boolean needToFlush = !pendingEvents.isEmpty(); + initializeUserUsageStatsServiceLocked(userId, System.currentTimeMillis(), + installedPackages); mUserUnlockedStates.put(userId, true); - final UserUsageStatsService userService = getUserDataAndInitializeIfNeededLocked( - userId, System.currentTimeMillis()); + final UserUsageStatsService userService = getUserUsageStatsServiceLocked(userId); if (userService == null) { Slog.i(TAG, "Attempted to unlock stopped or removed user " + userId); return; @@ -361,6 +366,29 @@ public class UsageStatsService extends SystemService implements } } + /** + * Fetches a map (package_name:install_time) of installed packages for the given user. This + * map contains all installed packages, including those packages which have been uninstalled + * with the DONT_DELETE_DATA flag. + * This is a helper method which should only be called when the given user's usage stats service + * is initialized; it performs a heavy query to package manager so do not call it otherwise. + * <br/> + * Note: DO NOT call this while holding the usage stats lock ({@code mLock}). + */ + private HashMap<String, Long> getInstalledPackages(int userId) { + if (mPackageManager == null) { + return null; + } + final List<PackageInfo> installedPackages = mPackageManager.getInstalledPackagesAsUser( + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + final HashMap<String, Long> packagesMap = new HashMap<>(); + for (int i = installedPackages.size() - 1; i >= 0; i--) { + final PackageInfo packageInfo = installedPackages.get(i); + packagesMap.put(packageInfo.packageName, packageInfo.firstInstallTime); + } + return packagesMap; + } + private DevicePolicyManagerInternal getDpmInternal() { if (mDpmInternal == null) { mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class); @@ -450,31 +478,42 @@ public class UsageStatsService extends SystemService implements } } - private UserUsageStatsService getUserDataAndInitializeIfNeededLocked(int userId, - long currentTimeMillis) { - UserUsageStatsService service = mUserState.get(userId); + /** + * This should the be only way to fetch the usage stats service for a specific user. + */ + private UserUsageStatsService getUserUsageStatsServiceLocked(int userId) { + final UserUsageStatsService service = mUserState.get(userId); if (service == null) { - final File usageStatsDir = new File(Environment.getDataSystemCeDirectory(userId), - "usagestats"); - service = new UserUsageStatsService(getContext(), userId, usageStatsDir, this); - if (mUserUnlockedStates.get(userId)) { - try { - service.init(currentTimeMillis); - mUserState.put(userId, service); - } catch (Exception e) { - if (mUserManager.isUserUnlocked(userId)) { - throw e; // rethrow exception - user is unlocked - } else { - Slog.w(TAG, "Attempted to initialize service for " - + "stopped or removed user " + userId); - return null; - } - } - } + Slog.wtf(TAG, "Failed to fetch usage stats service for user " + userId + ". " + + "The user might not have been initialized yet."); } return service; } + /** + * Initializes the given user's usage stats service - this should ideally only be called once, + * when the user is initially unlocked. + */ + private void initializeUserUsageStatsServiceLocked(int userId, + long currentTimeMillis, HashMap<String, Long> installedPackages) { + final File usageStatsDir = new File(Environment.getDataSystemCeDirectory(userId), + "usagestats"); + final UserUsageStatsService service = new UserUsageStatsService(getContext(), userId, + usageStatsDir, this); + try { + service.init(currentTimeMillis, installedPackages); + mUserState.put(userId, service); + } catch (Exception e) { + if (mUserManager.isUserUnlocked(userId)) { + Slog.w(TAG, "Failed to initialized unlocked user " + userId); + throw e; // rethrow the exception - user is unlocked + } else { + Slog.w(TAG, "Attempted to initialize service for stopped or removed user " + + userId); + } + } + } + private void migrateStatsToSystemCeIfNeededLocked(int userId) { final File usageStatsDir = new File(Environment.getDataSystemCeDirectory(userId), "usagestats"); @@ -694,7 +733,6 @@ public class UsageStatsService extends SystemService implements return; } - final long timeNow = System.currentTimeMillis(); final long elapsedRealtime = SystemClock.elapsedRealtime(); if (event.mPackage != null @@ -789,8 +827,7 @@ public class UsageStatsService extends SystemService implements break; } - final UserUsageStatsService service = - getUserDataAndInitializeIfNeededLocked(userId, timeNow); + final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId); if (service == null) { return; // user was stopped or removed } @@ -841,15 +878,18 @@ public class UsageStatsService extends SystemService implements mAppStandby.onUserRemoved(userId); mAppTimeLimit.onUserRemoved(userId); } + // Cancel any scheduled jobs for this user since the user is being removed. + UsageStatsIdleService.cancelJob(getContext(), userId); } /** * Called by the Handler for message MSG_PACKAGE_REMOVED. */ private void onPackageRemoved(int userId, String packageName) { + final int tokenRemoved; synchronized (mLock) { final long timeRemoved = System.currentTimeMillis(); - if (!mUserUnlockedStates.get(userId, false)) { + if (!mUserUnlockedStates.get(userId)) { // If user is not unlocked and a package is removed for them, we will handle it // when the user service is initialized and package manager is queried. return; @@ -859,7 +899,30 @@ public class UsageStatsService extends SystemService implements return; } - userService.onPackageRemoved(packageName, timeRemoved); + tokenRemoved = userService.onPackageRemoved(packageName, timeRemoved); + } + + // Schedule a job to prune any data related to this package. + if (tokenRemoved != PackagesTokenData.UNASSIGNED_TOKEN) { + UsageStatsIdleService.scheduleJob(getContext(), userId); + } + } + + /** + * Called by the Binder stub. + */ + private boolean pruneUninstalledPackagesData(int userId) { + synchronized (mLock) { + if (!mUserUnlockedStates.get(userId)) { + return false; // user is no longer unlocked + } + + final UserUsageStatsService userService = mUserState.get(userId); + if (userService == null) { + return false; // user was stopped or removed + } + + return userService.pruneUninstalledPackagesData(); } } @@ -874,8 +937,7 @@ public class UsageStatsService extends SystemService implements return null; } - final UserUsageStatsService service = - getUserDataAndInitializeIfNeededLocked(userId, System.currentTimeMillis()); + final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId); if (service == null) { return null; // user was stopped or removed } @@ -909,8 +971,7 @@ public class UsageStatsService extends SystemService implements return null; } - final UserUsageStatsService service = - getUserDataAndInitializeIfNeededLocked(userId, System.currentTimeMillis()); + final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId); if (service == null) { return null; // user was stopped or removed } @@ -929,8 +990,7 @@ public class UsageStatsService extends SystemService implements return null; } - final UserUsageStatsService service = - getUserDataAndInitializeIfNeededLocked(userId, System.currentTimeMillis()); + final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId); if (service == null) { return null; // user was stopped or removed } @@ -949,8 +1009,7 @@ public class UsageStatsService extends SystemService implements return null; } - final UserUsageStatsService service = - getUserDataAndInitializeIfNeededLocked(userId, System.currentTimeMillis()); + final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId); if (service == null) { return null; // user was stopped or removed } @@ -969,8 +1028,7 @@ public class UsageStatsService extends SystemService implements return null; } - final UserUsageStatsService service = - getUserDataAndInitializeIfNeededLocked(userId, System.currentTimeMillis()); + final UserUsageStatsService service = getUserUsageStatsServiceLocked(userId); if (service == null) { return null; // user was stopped or removed } @@ -2025,8 +2083,7 @@ public class UsageStatsService extends SystemService implements // Check to ensure that only user 0's data is b/r for now if (user == UserHandle.USER_SYSTEM) { - final UserUsageStatsService userStats = getUserDataAndInitializeIfNeededLocked( - user, System.currentTimeMillis()); + final UserUsageStatsService userStats = getUserUsageStatsServiceLocked(user); if (userStats == null) { return null; // user was stopped or removed } @@ -2046,8 +2103,7 @@ public class UsageStatsService extends SystemService implements } if (user == UserHandle.USER_SYSTEM) { - final UserUsageStatsService userStats = getUserDataAndInitializeIfNeededLocked( - user, System.currentTimeMillis()); + final UserUsageStatsService userStats = getUserUsageStatsServiceLocked(user); if (userStats == null) { return; // user was stopped or removed } @@ -2108,6 +2164,11 @@ public class UsageStatsService extends SystemService implements public AppUsageLimitData getAppUsageLimit(String packageName, UserHandle user) { return mAppTimeLimit.getAppUsageLimit(packageName, user); } + + @Override + public boolean pruneUninstalledPackagesData(int userId) { + return UsageStatsService.this.pruneUninstalledPackagesData(userId); + } } private class MyPackageMonitor extends PackageMonitor { diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index c6a5fcfa8d2c..179b6490a7fd 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -34,10 +34,7 @@ import android.app.usage.UsageEvents.Event; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManagerInternal; import android.content.res.Configuration; -import android.os.Process; import android.os.SystemClock; import android.text.format.DateUtils; import android.util.ArrayMap; @@ -46,8 +43,8 @@ import android.util.AtomicFile; import android.util.Slog; import android.util.SparseIntArray; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.IndentingPrintWriter; -import com.android.server.LocalServices; import com.android.server.usage.UsageStatsDatabase.StatCombiner; import java.io.File; @@ -55,7 +52,7 @@ import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; /** @@ -112,9 +109,12 @@ class UserUsageStatsService { mSystemTimeSnapshot = System.currentTimeMillis(); } - void init(final long currentTimeMillis) { - readPackageMappingsLocked(); + void init(final long currentTimeMillis, HashMap<String, Long> installedPackages) { + readPackageMappingsLocked(installedPackages); mDatabase.init(currentTimeMillis); + if (mDatabase.wasUpgradePerformed()) { + mDatabase.prunePackagesDataOnUpgrade(installedPackages); + } int nullCount = 0; for (int i = 0; i < mCurrentStats.length; i++) { @@ -170,52 +170,53 @@ class UserUsageStatsService { persistActiveStats(); } - void onPackageRemoved(String packageName, long timeRemoved) { - mDatabase.onPackageRemoved(packageName, timeRemoved); + int onPackageRemoved(String packageName, long timeRemoved) { + return mDatabase.onPackageRemoved(packageName, timeRemoved); } - private void readPackageMappingsLocked() { + private void readPackageMappingsLocked(HashMap<String, Long> installedPackages) { mDatabase.readMappingsLocked(); - cleanUpPackageMappingsLocked(); + updatePackageMappingsLocked(installedPackages); } /** - * Queries Package Manager for a list of installed packages and removes those packages from - * mPackagesTokenData which are not installed any more. + * Queries Job Scheduler for any pending data prune jobs and if any exist, it updates the + * package mappings in memory by removing those tokens. * This will only happen once per device boot, when the user is unlocked for the first time. + * + * @param installedPackages map of installed packages (package_name:package_install_time) */ - private void cleanUpPackageMappingsLocked() { - final long timeNow = System.currentTimeMillis(); - /* - Note (b/142501248): PackageManagerInternal#getInstalledApplications is not lightweight. - Once its implementation is updated, or it's replaced with a better alternative, update - the call here to use it. For now, using the heavy #getInstalledApplications is okay since - this clean-up is only performed once every boot. - */ - final PackageManagerInternal packageManagerInternal = - LocalServices.getService(PackageManagerInternal.class); - if (packageManagerInternal == null) { + private void updatePackageMappingsLocked(HashMap<String, Long> installedPackages) { + if (ArrayUtils.isEmpty(installedPackages)) { return; } - final List<ApplicationInfo> installedPackages = - packageManagerInternal.getInstalledApplications(0, mUserId, Process.SYSTEM_UID); - // convert the package list to a set for easy look-ups - final HashSet<String> packagesSet = new HashSet<>(installedPackages.size()); - for (int i = installedPackages.size() - 1; i >= 0; i--) { - packagesSet.add(installedPackages.get(i).packageName); - } - final List<String> removedPackages = new ArrayList<>(); + + final long timeNow = System.currentTimeMillis(); + final ArrayList<String> removedPackages = new ArrayList<>(); // populate list of packages that are found in the mappings but not in the installed list for (int i = mDatabase.mPackagesTokenData.packagesToTokensMap.size() - 1; i >= 0; i--) { - if (!packagesSet.contains(mDatabase.mPackagesTokenData.packagesToTokensMap.keyAt(i))) { - removedPackages.add(mDatabase.mPackagesTokenData.packagesToTokensMap.keyAt(i)); + final String packageName = mDatabase.mPackagesTokenData.packagesToTokensMap.keyAt(i); + if (!installedPackages.containsKey(packageName)) { + removedPackages.add(packageName); } } + if (removedPackages.isEmpty()) { + return; + } - // remove packages in the mappings that are no longer installed + // remove packages in the mappings that are no longer installed and persist to disk for (int i = removedPackages.size() - 1; i >= 0; i--) { mDatabase.mPackagesTokenData.removePackage(removedPackages.get(i), timeNow); } + try { + mDatabase.writeMappingsLocked(); + } catch (Exception e) { + Slog.w(TAG, "Unable to write updated package mappings file on service initialization."); + } + } + + boolean pruneUninstalledPackagesData() { + return mDatabase.pruneUninstalledPackagesData(); } private void onTimeChanged(long oldTime, long newTime) { |