• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.watchdog;
18 
19 import static com.android.car.watchdog.TimeSource.ZONE_OFFSET;
20 
21 import android.annotation.IntDef;
22 import android.annotation.Nullable;
23 import android.annotation.UserIdInt;
24 import android.automotive.watchdog.PerStateBytes;
25 import android.car.builtin.util.Slogf;
26 import android.car.watchdog.IoOveruseStats;
27 import android.car.watchdog.PackageKillableState.KillableState;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.database.Cursor;
31 import android.database.SQLException;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.database.sqlite.SQLiteOpenHelper;
34 import android.os.Process;
35 import android.util.ArrayMap;
36 import android.util.ArraySet;
37 import android.util.SparseArray;
38 
39 import com.android.car.CarLog;
40 import com.android.car.internal.util.IntArray;
41 import com.android.internal.annotations.GuardedBy;
42 import com.android.internal.annotations.VisibleForTesting;
43 
44 import java.io.File;
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 import java.time.Instant;
48 import java.time.Period;
49 import java.time.ZonedDateTime;
50 import java.time.temporal.ChronoUnit;
51 import java.time.temporal.TemporalUnit;
52 import java.util.ArrayList;
53 import java.util.Comparator;
54 import java.util.List;
55 import java.util.Locale;
56 import java.util.Objects;
57 
58 /**
59  * Defines the database to store/retrieve system resource stats history from local storage.
60  */
61 public final class WatchdogStorage {
62     private static final String TAG = CarLog.tagFor(WatchdogStorage.class);
63     private static final int RETENTION_PERIOD_IN_DAYS = 30;
64 
65     /**
66      * The database is clean when it is synchronized with the in-memory cache. Cannot start a
67      * write while in this state.
68      */
69     private static final int DB_STATE_CLEAN = 1;
70 
71     /**
72      * The database is dirty when it is not synchronized with the in-memory cache. When the
73      * database is in this state, no write is in progress.
74      */
75     private static final int DB_STATE_DIRTY = 2;
76 
77     /**
78      * Database write in progress. Cannot start a new write when the database is in this state.
79      */
80     private static final int DB_STATE_WRITE_IN_PROGRESS = 3;
81 
82     /**
83      * The database enters this state when the database is marked dirty while a write is in
84      * progress.
85      */
86     private static final int DB_STATE_WRITE_IN_PROGRESS_DIRTY = 4;
87 
88     @Retention(RetentionPolicy.SOURCE)
89     @IntDef(prefix = {"DB_STATE_"}, value = {
90             DB_STATE_CLEAN,
91             DB_STATE_DIRTY,
92             DB_STATE_WRITE_IN_PROGRESS,
93             DB_STATE_WRITE_IN_PROGRESS_DIRTY
94     })
95     private @interface DatabaseStateType{}
96 
97     public static final int FAILED_TRANSACTION = -1;
98     /* Stats are stored on a daily basis. */
99     public static final TemporalUnit STATS_TEMPORAL_UNIT = ChronoUnit.DAYS;
100     /* Number of days to retain the stats in local storage. */
101     public static final Period RETENTION_PERIOD =
102             Period.ofDays(RETENTION_PERIOD_IN_DAYS).normalized();
103     public static final String ZONE_MODIFIER = "utc";
104     public static final String DATE_MODIFIER = "unixepoch";
105 
106     private final WatchdogDbHelper mDbHelper;
107     private final ArrayMap<String, UserPackage> mUserPackagesByKey = new ArrayMap<>();
108     private final ArrayMap<String, UserPackage> mUserPackagesById = new ArrayMap<>();
109     private TimeSource mTimeSource;
110     private final Object mLock = new Object();
111     // Cache of today's I/O overuse stats collected during the previous boot. The data contained in
112     // the cache won't change until the next boot, so it is safe to cache the data in memory.
113     @GuardedBy("mLock")
114     private final List<IoUsageStatsEntry> mTodayIoUsageStatsEntries = new ArrayList<>();
115     @GuardedBy("mLock")
116     private @DatabaseStateType int mCurrentDbState = DB_STATE_CLEAN;
117 
WatchdogStorage(Context context, TimeSource timeSource)118     public WatchdogStorage(Context context, TimeSource timeSource) {
119         this(context, /* useDataSystemCarDir= */ true, timeSource);
120     }
121 
122     @VisibleForTesting
WatchdogStorage(Context context, boolean useDataSystemCarDir, TimeSource timeSource)123     WatchdogStorage(Context context, boolean useDataSystemCarDir, TimeSource timeSource) {
124         mTimeSource = timeSource;
125         mDbHelper = new WatchdogDbHelper(context, useDataSystemCarDir, mTimeSource);
126     }
127 
128     /** Releases resources. */
release()129     public void release() {
130         mDbHelper.close();
131     }
132 
133     /** Handles database shrink. */
shrinkDatabase()134     public void shrinkDatabase() {
135         try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
136             mDbHelper.onShrink(db);
137         }
138     }
139 
140     /**
141      * Marks the database as dirty. The database is dirty when it is not synchronized with the
142      * memory cache.
143      */
markDirty()144     public void markDirty() {
145         synchronized (mLock) {
146             mCurrentDbState = mCurrentDbState == DB_STATE_WRITE_IN_PROGRESS
147                     ? DB_STATE_WRITE_IN_PROGRESS_DIRTY : DB_STATE_DIRTY;
148         }
149         Slogf.i(TAG, "Database marked dirty.");
150     }
151 
152     /**
153      * Starts write to database only if database is dirty and no writing is in progress.
154      *
155      * @return {@code true} if start was successful, otherwise {@code false}.
156      */
startWrite()157     public boolean startWrite() {
158         synchronized (mLock) {
159             if (mCurrentDbState != DB_STATE_DIRTY) {
160                 Slogf.e(TAG, "Cannot start a new write while the DB state is %s",
161                         toDbStateString(mCurrentDbState));
162                 return false;
163             }
164             mCurrentDbState = DB_STATE_WRITE_IN_PROGRESS;
165             return true;
166         }
167     }
168 
169     /** Ends write to database if write is in progress. */
endWrite()170     public void endWrite() {
171         synchronized (mLock) {
172             mCurrentDbState = mCurrentDbState == DB_STATE_CLEAN ? DB_STATE_CLEAN : DB_STATE_DIRTY;
173         }
174     }
175 
176     /** Marks the database as clean during an in progress database write. */
markWriteSuccessful()177     public void markWriteSuccessful() {
178         synchronized (mLock) {
179             if (mCurrentDbState != DB_STATE_WRITE_IN_PROGRESS) {
180                 Slogf.e(TAG, "Failed to mark write successful as the current db state is %s",
181                         toDbStateString(mCurrentDbState));
182                 return;
183             }
184             mCurrentDbState = DB_STATE_CLEAN;
185         }
186     }
187 
188     /** Saves the given user package settings entries and returns whether the change succeeded. */
saveUserPackageSettings(List<UserPackageSettingsEntry> entries)189     public boolean saveUserPackageSettings(List<UserPackageSettingsEntry> entries) {
190         ArraySet<Integer> usersWithMissingIds = new ArraySet<>();
191         boolean isWriteSuccessful = false;
192         try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
193             try {
194                 db.beginTransaction();
195                 for (int i = 0; i < entries.size(); ++i) {
196                     UserPackageSettingsEntry entry = entries.get(i);
197                     // Note: DO NOT replace existing entries in the UserPackageSettingsTable because
198                     // the replace operation deletes the old entry and inserts a new entry in the
199                     // table. This deletes the entries (in other tables) that are associated with
200                     // the old userPackageId. And also the userPackageId is auto-incremented.
201                     if (mUserPackagesByKey.get(UserPackage.getKey(entry.userId, entry.packageName))
202                             != null && UserPackageSettingsTable.updateEntry(db, entry)) {
203                         continue;
204                     }
205                     usersWithMissingIds.add(entry.userId);
206                     if (!UserPackageSettingsTable.replaceEntry(db, entry)) {
207                         return false;
208                     }
209                 }
210                 db.setTransactionSuccessful();
211                 isWriteSuccessful = true;
212             } finally {
213                 db.endTransaction();
214             }
215             populateUserPackages(db, usersWithMissingIds);
216         }
217         return isWriteSuccessful;
218     }
219 
220     /** Returns the user package setting entries. */
getUserPackageSettings()221     public List<UserPackageSettingsEntry> getUserPackageSettings() {
222         ArrayMap<String, UserPackageSettingsEntry> entriesById;
223         try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
224             entriesById = UserPackageSettingsTable.querySettings(db);
225         }
226         List<UserPackageSettingsEntry> entries = new ArrayList<>(entriesById.size());
227         for (int i = 0; i < entriesById.size(); ++i) {
228             String userPackageId = entriesById.keyAt(i);
229             UserPackageSettingsEntry entry = entriesById.valueAt(i);
230             UserPackage userPackage = new UserPackage(userPackageId, entry.userId,
231                     entry.packageName);
232             mUserPackagesByKey.put(userPackage.getKey(), userPackage);
233             mUserPackagesById.put(userPackage.userPackageId, userPackage);
234             entries.add(entry);
235         }
236         return entries;
237     }
238 
239     /**
240      * Saves the given I/O usage stats.
241      *
242      * @return the number of saved entries, on success. Otherwise, returns
243      *     {@code FAILED_TRANSACTION}
244      */
saveIoUsageStats(List<IoUsageStatsEntry> entries)245     public int saveIoUsageStats(List<IoUsageStatsEntry> entries) {
246         return saveIoUsageStats(entries, /* shouldCheckRetention= */ true);
247     }
248 
249     /** Returns the saved I/O usage stats for the current day. */
getTodayIoUsageStats()250     public List<IoUsageStatsEntry> getTodayIoUsageStats() {
251         synchronized (mLock) {
252             if (!mTodayIoUsageStatsEntries.isEmpty()) {
253                 return new ArrayList<>(mTodayIoUsageStatsEntries);
254             }
255             long includingStartEpochSeconds = mTimeSource.getCurrentDate().toEpochSecond();
256             long excludingEndEpochSeconds = mTimeSource.getCurrentDateTime().toEpochSecond();
257             ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsagesById;
258             try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
259                 ioUsagesById = IoUsageStatsTable.queryStats(db, includingStartEpochSeconds,
260                         excludingEndEpochSeconds);
261             }
262             for (int i = 0; i < ioUsagesById.size(); ++i) {
263                 String userPackageId = ioUsagesById.keyAt(i);
264                 UserPackage userPackage = mUserPackagesById.get(userPackageId);
265                 if (userPackage == null) {
266                     Slogf.i(TAG,
267                             "Failed to find user id and package name for user package id: '%s'",
268                             userPackageId);
269                     continue;
270                 }
271                 mTodayIoUsageStatsEntries.add(new IoUsageStatsEntry(
272                         userPackage.userId, userPackage.packageName,
273                         ioUsagesById.valueAt(i)));
274             }
275             return new ArrayList<>(mTodayIoUsageStatsEntries);
276         }
277     }
278 
279     /** Deletes user package settings and resource overuse stats. */
deleteUserPackage(@serIdInt int userId, String packageName)280     public void deleteUserPackage(@UserIdInt int userId, String packageName) {
281         UserPackage userPackage = mUserPackagesByKey.get(UserPackage.getKey(userId, packageName));
282         if (userPackage == null) {
283             Slogf.w(TAG, "Failed to find user package id for user id '%d' and package '%s",
284                     userId, packageName);
285             return;
286         }
287         mUserPackagesByKey.remove(userPackage.getKey());
288         mUserPackagesById.remove(userPackage.userPackageId);
289         try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
290             UserPackageSettingsTable.deleteUserPackage(db, userId, packageName);
291         }
292     }
293 
294     /**
295      * Returns the aggregated historical I/O overuse stats for the given user package or
296      * {@code null} when stats are not available.
297      */
298     @Nullable
getHistoricalIoOveruseStats(@serIdInt int userId, String packageName, int numDaysAgo)299     public IoOveruseStats getHistoricalIoOveruseStats(@UserIdInt int userId, String packageName,
300             int numDaysAgo) {
301         ZonedDateTime currentDate = mTimeSource.getCurrentDate();
302         long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond();
303         long excludingEndEpochSeconds = currentDate.toEpochSecond();
304         try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
305             UserPackage userPackage = mUserPackagesByKey.get(
306                     UserPackage.getKey(userId, packageName));
307             if (userPackage == null) {
308                 /* Packages without historical stats don't have userPackage entry. */
309                 return null;
310             }
311             return IoUsageStatsTable.queryIoOveruseStatsForUserPackageId(db,
312                     userPackage.userPackageId, includingStartEpochSeconds,
313                     excludingEndEpochSeconds);
314         }
315     }
316 
317     /**
318      * Returns daily system-level I/O usage summaries for the given period or {@code null} when
319      * summaries are not available.
320      */
getDailySystemIoUsageSummaries( long minSystemTotalWrittenBytes, long includingStartEpochSeconds, long excludingEndEpochSeconds)321     public @Nullable List<AtomsProto.CarWatchdogDailyIoUsageSummary> getDailySystemIoUsageSummaries(
322             long minSystemTotalWrittenBytes, long includingStartEpochSeconds,
323             long excludingEndEpochSeconds) {
324         try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
325             List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries =
326                     IoUsageStatsTable.queryDailySystemIoUsageSummaries(db,
327                             includingStartEpochSeconds, excludingEndEpochSeconds);
328             if (dailyIoUsageSummaries == null) {
329                 return null;
330             }
331             long systemTotalWrittenBytes = 0;
332             for (int i = 0; i < dailyIoUsageSummaries.size(); i++) {
333                 AtomsProto.CarWatchdogPerStateBytes writtenBytes =
334                         dailyIoUsageSummaries.get(i).getWrittenBytes();
335                 systemTotalWrittenBytes += writtenBytes.getForegroundBytes()
336                         + writtenBytes.getBackgroundBytes() + writtenBytes.getGarageModeBytes();
337             }
338             if (systemTotalWrittenBytes < minSystemTotalWrittenBytes) {
339                 return null;
340             }
341             return dailyIoUsageSummaries;
342         }
343     }
344 
345     /**
346      * Returns top N disk I/O users' daily I/O usage summaries for the given period or {@code null}
347      * when summaries are not available.
348      */
getTopUsersDailyIoUsageSummaries( int numTopUsers, long minSystemTotalWrittenBytes, long includingStartEpochSeconds, long excludingEndEpochSeconds)349     public @Nullable List<UserPackageDailySummaries> getTopUsersDailyIoUsageSummaries(
350             int numTopUsers, long minSystemTotalWrittenBytes, long includingStartEpochSeconds,
351             long excludingEndEpochSeconds) {
352         ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>> summariesById;
353         try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
354             long systemTotalWrittenBytes = IoUsageStatsTable.querySystemTotalWrittenBytes(db,
355                     includingStartEpochSeconds, excludingEndEpochSeconds);
356             if (systemTotalWrittenBytes < minSystemTotalWrittenBytes) {
357                 return null;
358             }
359             summariesById = IoUsageStatsTable.queryTopUsersDailyIoUsageSummaries(db,
360                     numTopUsers, includingStartEpochSeconds, excludingEndEpochSeconds);
361         }
362         if (summariesById == null) {
363             return null;
364         }
365         ArrayList<UserPackageDailySummaries> userPackageDailySummaries = new ArrayList<>();
366         for (int i = 0; i < summariesById.size(); ++i) {
367             String id = summariesById.keyAt(i);
368             UserPackage userPackage = mUserPackagesById.get(id);
369             if (userPackage == null) {
370                 Slogf.i(TAG,
371                         "Failed to find user id and package name for user package id: '%s'",
372                         id);
373                 continue;
374             }
375             userPackageDailySummaries.add(new UserPackageDailySummaries(userPackage.userId,
376                     userPackage.packageName, summariesById.valueAt(i)));
377         }
378         userPackageDailySummaries
379                 .sort(Comparator.comparingLong(UserPackageDailySummaries::getTotalWrittenBytes)
380                         .reversed());
381         return userPackageDailySummaries;
382     }
383 
384     /**
385      * Returns the aggregated historical overuses minus the forgiven overuses for all saved
386      * packages. Forgiven overuses are overuses that have been attributed previously to a package's
387      * recurring overuse.
388      */
getNotForgivenHistoricalIoOveruses(int numDaysAgo)389     public List<NotForgivenOverusesEntry> getNotForgivenHistoricalIoOveruses(int numDaysAgo) {
390         ZonedDateTime currentDate =
391                 mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
392         long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond();
393         long excludingEndEpochSeconds = currentDate.toEpochSecond();
394         ArrayMap<String, Integer> notForgivenOverusesById;
395         try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
396             notForgivenOverusesById = IoUsageStatsTable.queryNotForgivenHistoricalOveruses(db,
397                     includingStartEpochSeconds, excludingEndEpochSeconds);
398         }
399         List<NotForgivenOverusesEntry> notForgivenOverusesEntries = new ArrayList<>();
400         for (int i = 0; i < notForgivenOverusesById.size(); i++) {
401             String id = notForgivenOverusesById.keyAt(i);
402             UserPackage userPackage = mUserPackagesById.get(id);
403             if (userPackage == null) {
404                 Slogf.w(TAG,
405                         "Failed to find user id and package name for user package id: '%s'",
406                         id);
407                 continue;
408             }
409             notForgivenOverusesEntries.add(new NotForgivenOverusesEntry(userPackage.userId,
410                     userPackage.packageName, notForgivenOverusesById.valueAt(i)));
411         }
412         return notForgivenOverusesEntries;
413     }
414 
415     /**
416      * Forgives all historical overuses between yesterday and {@code numDaysAgo}
417      * for a list of specific {@code userIds} and {@code packageNames}.
418      */
forgiveHistoricalOveruses(SparseArray<List<String>> packagesByUserId, int numDaysAgo)419     public void forgiveHistoricalOveruses(SparseArray<List<String>> packagesByUserId,
420             int numDaysAgo) {
421         if (packagesByUserId.size() == 0) {
422             Slogf.w(TAG, "No I/O usage stats provided to forgive historical overuses.");
423             return;
424         }
425         ZonedDateTime currentDate =
426                 mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
427         long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond();
428         long excludingEndEpochSeconds = currentDate.toEpochSecond();
429         List<String> userPackageIds = new ArrayList<>();
430         for (int i = 0; i < packagesByUserId.size(); i++) {
431             int userId = packagesByUserId.keyAt(i);
432             List<String> packages = packagesByUserId.valueAt(i);
433             for (int pkgIdx = 0; pkgIdx < packages.size(); pkgIdx++) {
434                 UserPackage userPackage =
435                         mUserPackagesByKey.get(UserPackage.getKey(userId, packages.get(pkgIdx)));
436                 if (userPackage == null) {
437                     // Packages without historical stats don't have userPackage entry.
438                     continue;
439                 }
440                 userPackageIds.add(userPackage.userPackageId);
441             }
442         }
443         try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
444             IoUsageStatsTable.forgiveHistoricalOverusesForPackage(db, userPackageIds,
445                     includingStartEpochSeconds, excludingEndEpochSeconds);
446         }
447     }
448 
449     /**
450      * Deletes all user package settings and resource stats for all non-alive users.
451      *
452      * @param aliveUserIds Array of alive user ids.
453      */
syncUsers(int[] aliveUserIds)454     public void syncUsers(int[] aliveUserIds) {
455         IntArray aliveUsers = IntArray.wrap(aliveUserIds);
456         for (int i = mUserPackagesByKey.size() - 1; i >= 0; --i) {
457             UserPackage userPackage = mUserPackagesByKey.valueAt(i);
458             if (aliveUsers.indexOf(userPackage.userId) == -1) {
459                 mUserPackagesByKey.removeAt(i);
460                 mUserPackagesById.remove(userPackage.userPackageId);
461             }
462         }
463         try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
464             UserPackageSettingsTable.syncUserPackagesWithAliveUsers(db, aliveUsers);
465         }
466     }
467 
468     @VisibleForTesting
saveIoUsageStats(List<IoUsageStatsEntry> entries, boolean shouldCheckRetention)469     int saveIoUsageStats(List<IoUsageStatsEntry> entries, boolean shouldCheckRetention) {
470         ZonedDateTime currentDate = mTimeSource.getCurrentDate();
471         List<ContentValues> rows = new ArrayList<>(entries.size());
472         for (int i = 0; i < entries.size(); ++i) {
473             IoUsageStatsEntry entry = entries.get(i);
474             UserPackage userPackage = mUserPackagesByKey.get(
475                     UserPackage.getKey(entry.userId, entry.packageName));
476             if (userPackage == null) {
477                 Slogf.i(TAG, "Failed to find user package id for user id '%d' and package '%s",
478                         entry.userId, entry.packageName);
479                 continue;
480             }
481             android.automotive.watchdog.IoOveruseStats ioOveruseStats =
482                     entry.ioUsage.getInternalIoOveruseStats();
483             ZonedDateTime statsDate = Instant.ofEpochSecond(ioOveruseStats.startTime)
484                     .atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
485             if (shouldCheckRetention && STATS_TEMPORAL_UNIT.between(statsDate, currentDate)
486                     >= RETENTION_PERIOD.get(STATS_TEMPORAL_UNIT)) {
487                 continue;
488             }
489             long statsDateEpochSeconds = statsDate.toEpochSecond();
490             rows.add(IoUsageStatsTable.getContentValues(
491                     userPackage.userPackageId, entry, statsDateEpochSeconds));
492         }
493         try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
494             return atomicReplaceEntries(db, IoUsageStatsTable.TABLE_NAME, rows);
495         }
496     }
497 
populateUserPackages(SQLiteDatabase db, ArraySet<Integer> users)498     private void populateUserPackages(SQLiteDatabase db, ArraySet<Integer> users) {
499         List<UserPackage> userPackages = UserPackageSettingsTable.queryUserPackages(db, users);
500         if (userPackages == null) {
501             return;
502         }
503         for (int i = 0; i < userPackages.size(); ++i) {
504             UserPackage userPackage = userPackages.get(i);
505             mUserPackagesByKey.put(userPackage.getKey(), userPackage);
506             mUserPackagesById.put(userPackage.userPackageId, userPackage);
507         }
508     }
509 
510     /**
511      * Atomically replace rows in a database table.
512      *
513      * @return the number of replaced entries, on success. Otherwise, returns
514      *     {@code FAILED_TRANSACTION}
515      */
atomicReplaceEntries(SQLiteDatabase db, String tableName, List<ContentValues> rows)516     private static int atomicReplaceEntries(SQLiteDatabase db, String tableName,
517             List<ContentValues> rows) {
518         if (rows.isEmpty()) {
519             return 0;
520         }
521         try {
522             db.beginTransaction();
523             for (int i = 0; i < rows.size(); ++i) {
524                 try {
525                     if (db.replaceOrThrow(tableName, null, rows.get(i)) == -1) {
526                         Slogf.e(TAG, "Failed to insert %s entry [%s]", tableName, rows.get(i));
527                         return FAILED_TRANSACTION;
528                     }
529                 } catch (SQLException e) {
530                     Slogf.e(TAG, e, "Failed to insert %s entry [%s]", tableName, rows.get(i));
531                     return FAILED_TRANSACTION;
532                 }
533             }
534             db.setTransactionSuccessful();
535         } finally {
536             db.endTransaction();
537         }
538         return rows.size();
539     }
540 
toDbStateString(int dbState)541     private static String toDbStateString(int dbState) {
542         switch (dbState) {
543             case DB_STATE_CLEAN:
544                 return "DB_STATE_CLEAN";
545             case DB_STATE_DIRTY:
546                 return "DB_STATE_DIRTY";
547             case DB_STATE_WRITE_IN_PROGRESS:
548                 return "DB_STATE_WRITE_IN_PROGRESS";
549             case DB_STATE_WRITE_IN_PROGRESS_DIRTY:
550                 return "DB_STATE_WRITE_IN_PROGRESS_DIRTY";
551             default:
552                 return "UNKNOWN";
553         }
554     }
555 
556     /** Defines the user package settings entry stored in the UserPackageSettingsTable. */
557     static final class UserPackageSettingsEntry {
558         public final @UserIdInt int userId;
559         public final String packageName;
560         public final @KillableState int killableState;
561 
UserPackageSettingsEntry(@serIdInt int userId, String packageName, @KillableState int killableState)562         UserPackageSettingsEntry(@UserIdInt int userId, String packageName,
563                 @KillableState int killableState) {
564             this.userId = userId;
565             this.packageName = packageName;
566             this.killableState = killableState;
567         }
568 
569         @Override
equals(Object obj)570         public boolean equals(Object obj) {
571             if (obj == this) {
572                 return true;
573             }
574             if (!(obj instanceof UserPackageSettingsEntry)) {
575                 return false;
576             }
577             UserPackageSettingsEntry other = (UserPackageSettingsEntry) obj;
578             return userId == other.userId && packageName.equals(other.packageName)
579                     && killableState == other.killableState;
580         }
581 
582         @Override
hashCode()583         public int hashCode() {
584             return Objects.hash(userId, packageName, killableState);
585         }
586 
587         @Override
toString()588         public String toString() {
589             return new StringBuilder().append("UserPackageSettingsEntry{userId: ").append(userId)
590                     .append(", packageName: ").append(packageName)
591                     .append(", killableState: ").append(killableState).append('}')
592                     .toString();
593         }
594     }
595 
596     /** Defines the daily summaries for user packages. */
597     static final class UserPackageDailySummaries {
598         public final @UserIdInt int userId;
599         public final String packageName;
600         public final List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries;
601         private final long mTotalWrittenBytes;
602 
UserPackageDailySummaries(@serIdInt int userId, String packageName, List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries)603         UserPackageDailySummaries(@UserIdInt int userId, String packageName,
604                 List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries) {
605             this.userId = userId;
606             this.packageName = packageName;
607             this.dailyIoUsageSummaries = dailyIoUsageSummaries;
608             this.mTotalWrittenBytes = computeTotalWrittenBytes();
609         }
610 
611         @Override
equals(Object obj)612         public boolean equals(Object obj) {
613             if (obj == this) {
614                 return true;
615             }
616             if (!(obj instanceof UserPackageDailySummaries)) {
617                 return false;
618             }
619             UserPackageDailySummaries other = (UserPackageDailySummaries) obj;
620             return userId == other.userId && packageName.equals(other.packageName)
621                     && dailyIoUsageSummaries.equals(other.dailyIoUsageSummaries);
622         }
623 
624         @Override
hashCode()625         public int hashCode() {
626             return Objects.hash(userId, packageName, dailyIoUsageSummaries, mTotalWrittenBytes);
627         }
628 
629         @Override
toString()630         public String toString() {
631             return new StringBuilder().append("UserPackageDailySummaries{userId: ").append(userId)
632                     .append(", packageName: ").append(packageName)
633                     .append(", dailyIoUsageSummaries: ").append(dailyIoUsageSummaries).append('}')
634                     .toString();
635         }
636 
getTotalWrittenBytes()637         long getTotalWrittenBytes() {
638             return mTotalWrittenBytes;
639         }
640 
computeTotalWrittenBytes()641         long computeTotalWrittenBytes() {
642             long totalBytes = 0;
643             for (int i = 0; i < dailyIoUsageSummaries.size(); ++i) {
644                 AtomsProto.CarWatchdogPerStateBytes writtenBytes =
645                         dailyIoUsageSummaries.get(i).getWrittenBytes();
646                 if (writtenBytes.hasForegroundBytes()) {
647                     totalBytes += writtenBytes.getForegroundBytes();
648                 }
649                 if (writtenBytes.hasBackgroundBytes()) {
650                     totalBytes += writtenBytes.getBackgroundBytes();
651                 }
652                 if (writtenBytes.hasGarageModeBytes()) {
653                     totalBytes += writtenBytes.getGarageModeBytes();
654                 }
655             }
656             return totalBytes;
657         }
658     }
659 
660     /**
661      * Defines the contents and queries for the user package settings table.
662      */
663     static final class UserPackageSettingsTable {
664         public static final String TABLE_NAME = "user_package_settings";
665         public static final String COLUMN_USER_PACKAGE_ID = "user_package_id";
666         public static final String COLUMN_PACKAGE_NAME = "package_name";
667         public static final String COLUMN_USER_ID = "user_id";
668         public static final String COLUMN_KILLABLE_STATE = "killable_state";
669 
createTable(SQLiteDatabase db)670         public static void createTable(SQLiteDatabase db) {
671             StringBuilder createCommand = new StringBuilder();
672             createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (")
673                     // Maximum value for COLUMN_USER_PACKAGE_ID is the max integer size supported by
674                     // the database. Thus, the number of entries that can be inserted into this
675                     // table is bound by this upper limit for the lifetime of the device
676                     // (i.e., Even when a userId is reused, the previous user_package_ids for
677                     // the corresponding userId won't be reused). When the IDs are exhausted,
678                     // any new insert operation will result in the error "database or disk is full".
679                     .append(COLUMN_USER_PACKAGE_ID).append(" INTEGER PRIMARY KEY AUTOINCREMENT, ")
680                     .append(COLUMN_PACKAGE_NAME).append(" TEXT NOT NULL, ")
681                     .append(COLUMN_USER_ID).append(" INTEGER NOT NULL, ")
682                     .append(COLUMN_KILLABLE_STATE).append(" INTEGER NOT NULL, ")
683                     .append("UNIQUE(").append(COLUMN_PACKAGE_NAME)
684                     .append(", ").append(COLUMN_USER_ID).append("))");
685             db.execSQL(createCommand.toString());
686             Slogf.i(TAG, "Successfully created the %s table in the %s database version %d",
687                     TABLE_NAME, WatchdogDbHelper.DATABASE_NAME, WatchdogDbHelper.DATABASE_VERSION);
688         }
689 
updateEntry(SQLiteDatabase db, UserPackageSettingsEntry entry)690         public static boolean updateEntry(SQLiteDatabase db, UserPackageSettingsEntry entry) {
691             ContentValues values = new ContentValues();
692             values.put(COLUMN_KILLABLE_STATE, entry.killableState);
693 
694             StringBuilder whereClause = new StringBuilder(COLUMN_PACKAGE_NAME).append(" = ? AND ")
695                             .append(COLUMN_USER_ID).append(" = ?");
696             String[] whereArgs = new String[]{entry.packageName, String.valueOf(entry.userId)};
697 
698             if (db.update(TABLE_NAME, values, whereClause.toString(), whereArgs) < 1) {
699                 Slogf.e(TAG, "Failed to update %d entry with package name: %s and user id: %d",
700                         TABLE_NAME, entry.packageName, entry.userId);
701                 return false;
702             }
703             return true;
704         }
705 
replaceEntry(SQLiteDatabase db, UserPackageSettingsEntry entry)706         public static boolean replaceEntry(SQLiteDatabase db, UserPackageSettingsEntry entry) {
707             ContentValues values = new ContentValues();
708             values.put(COLUMN_USER_ID, entry.userId);
709             values.put(COLUMN_PACKAGE_NAME, entry.packageName);
710             values.put(COLUMN_KILLABLE_STATE, entry.killableState);
711 
712             if (db.replaceOrThrow(UserPackageSettingsTable.TABLE_NAME, null, values) == -1) {
713                 Slogf.e(TAG, "Failed to replaced %s entry [%s]", TABLE_NAME, values);
714                 return false;
715             }
716             return true;
717         }
718 
querySettings(SQLiteDatabase db)719         public static ArrayMap<String, UserPackageSettingsEntry> querySettings(SQLiteDatabase db) {
720             StringBuilder queryBuilder = new StringBuilder();
721             queryBuilder.append("SELECT ")
722                     .append(COLUMN_USER_PACKAGE_ID).append(", ")
723                     .append(COLUMN_USER_ID).append(", ")
724                     .append(COLUMN_PACKAGE_NAME).append(", ")
725                     .append(COLUMN_KILLABLE_STATE)
726                     .append(" FROM ").append(TABLE_NAME);
727 
728             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), new String[]{})) {
729                 ArrayMap<String, UserPackageSettingsEntry> entriesById = new ArrayMap<>(
730                         cursor.getCount());
731                 while (cursor.moveToNext()) {
732                     entriesById.put(cursor.getString(0), new UserPackageSettingsEntry(
733                             cursor.getInt(1), cursor.getString(2), cursor.getInt(3)));
734                 }
735                 return entriesById;
736             }
737         }
738 
739         /**
740          * Returns the UserPackage entries for the given users. When no users are provided or no
741          * data returned by the DB query, returns null.
742          */
743         @Nullable
queryUserPackages(SQLiteDatabase db, ArraySet<Integer> users)744         public static List<UserPackage> queryUserPackages(SQLiteDatabase db,
745                 ArraySet<Integer> users) {
746             int numUsers = users.size();
747             if (numUsers == 0) {
748                 return null;
749             }
750             StringBuilder queryBuilder = new StringBuilder();
751             queryBuilder.append("SELECT ")
752                     .append(COLUMN_USER_PACKAGE_ID).append(", ")
753                     .append(COLUMN_USER_ID).append(", ")
754                     .append(COLUMN_PACKAGE_NAME)
755                     .append(" FROM ").append(TABLE_NAME)
756                     .append(" WHERE ").append(COLUMN_USER_ID).append(" IN (");
757             for (int i = 0; i < numUsers; ++i) {
758                 queryBuilder.append(users.valueAt(i));
759                 if (i < numUsers - 1) {
760                     queryBuilder.append(", ");
761                 }
762             }
763             queryBuilder.append(")");
764             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), new String[]{})) {
765                 if (cursor.getCount() == 0) {
766                     return null;
767                 }
768                 List<UserPackage> userPackages = new ArrayList<>(cursor.getCount());
769                 while (cursor.moveToNext()) {
770                     userPackages.add(new UserPackage(
771                             cursor.getString(0), cursor.getInt(1), cursor.getString(2)));
772                 }
773                 return userPackages;
774             }
775         }
776 
deleteUserPackage(SQLiteDatabase db, @UserIdInt int userId, String packageName)777         public static void deleteUserPackage(SQLiteDatabase db, @UserIdInt int userId,
778                 String packageName) {
779             String whereClause = COLUMN_USER_ID + "= ? and " + COLUMN_PACKAGE_NAME + "= ?";
780             String[] whereArgs = new String[]{String.valueOf(userId), packageName};
781             int deletedRows = db.delete(TABLE_NAME, whereClause, whereArgs);
782             Slogf.i(TAG, "Deleted %d user package settings db rows for user %d and package %s",
783                     deletedRows, userId, packageName);
784         }
785 
syncUserPackagesWithAliveUsers(SQLiteDatabase db, IntArray aliveUsers)786         public static void syncUserPackagesWithAliveUsers(SQLiteDatabase db, IntArray aliveUsers) {
787             StringBuilder queryBuilder = new StringBuilder();
788             for (int i = 0; i < aliveUsers.size(); ++i) {
789                 if (i == 0) {
790                     queryBuilder.append(COLUMN_USER_ID).append(" NOT IN (");
791                 } else {
792                     queryBuilder.append(", ");
793                 }
794                 queryBuilder.append(aliveUsers.get(i));
795                 if (i == aliveUsers.size() - 1) {
796                     queryBuilder.append(")");
797                 }
798             }
799             int deletedRows = db.delete(TABLE_NAME, queryBuilder.toString(), new String[]{});
800             Slogf.i(TAG, "Deleted %d user package settings db rows while syncing with alive users",
801                     deletedRows);
802         }
803     }
804 
805     /** Defines the I/O usage entry stored in the IoUsageStatsTable. */
806     static final class IoUsageStatsEntry {
807         public final @UserIdInt int userId;
808         public final String packageName;
809         public final WatchdogPerfHandler.PackageIoUsage ioUsage;
810 
IoUsageStatsEntry(@serIdInt int userId, String packageName, WatchdogPerfHandler.PackageIoUsage ioUsage)811         IoUsageStatsEntry(@UserIdInt int userId,
812                 String packageName, WatchdogPerfHandler.PackageIoUsage ioUsage) {
813             this.userId = userId;
814             this.packageName = packageName;
815             this.ioUsage = ioUsage;
816         }
817     }
818 
819     /** Defines the not forgiven overuses stored in the IoUsageStatsTable. */
820     static final class NotForgivenOverusesEntry {
821         public final @UserIdInt int userId;
822         public final String packageName;
823         public final int notForgivenOveruses;
824 
NotForgivenOverusesEntry(@serIdInt int userId, String packageName, int notForgivenOveruses)825         NotForgivenOverusesEntry(@UserIdInt int userId,
826                 String packageName, int notForgivenOveruses) {
827             this.userId = userId;
828             this.packageName = packageName;
829             this.notForgivenOveruses = notForgivenOveruses;
830         }
831 
832         @Override
equals(Object obj)833         public boolean equals(Object obj) {
834             if (this == obj) {
835                 return true;
836             }
837             if (!(obj instanceof NotForgivenOverusesEntry)) {
838                 return false;
839             }
840             NotForgivenOverusesEntry other = (NotForgivenOverusesEntry) obj;
841             return userId == other.userId
842                     && packageName.equals(other.packageName)
843                     && notForgivenOveruses == other.notForgivenOveruses;
844         }
845 
846         @Override
hashCode()847         public int hashCode() {
848             return Objects.hash(userId, packageName, notForgivenOveruses);
849         }
850 
851         @Override
toString()852         public String toString() {
853             return "NotForgivenOverusesEntry {UserId: " + userId
854                     + ", Package name: " + packageName
855                     + ", Not forgiven overuses: " + notForgivenOveruses + "}";
856         }
857     }
858 
859     /**
860      * Defines the contents and queries for the I/O usage stats table.
861      */
862     static final class IoUsageStatsTable {
863         public static final String TABLE_NAME = "io_usage_stats";
864         public static final String COLUMN_USER_PACKAGE_ID = "user_package_id";
865         public static final String COLUMN_DATE_EPOCH = "date_epoch";
866         public static final String COLUMN_NUM_OVERUSES = "num_overuses";
867         public static final String COLUMN_NUM_FORGIVEN_OVERUSES =  "num_forgiven_overuses";
868         public static final String COLUMN_NUM_TIMES_KILLED = "num_times_killed";
869         public static final String COLUMN_WRITTEN_FOREGROUND_BYTES = "written_foreground_bytes";
870         public static final String COLUMN_WRITTEN_BACKGROUND_BYTES = "written_background_bytes";
871         public static final String COLUMN_WRITTEN_GARAGE_MODE_BYTES = "written_garage_mode_bytes";
872         /* Below columns will be null for historical stats i.e., when the date != current date. */
873         public static final String COLUMN_REMAINING_FOREGROUND_WRITE_BYTES =
874                 "remaining_foreground_write_bytes";
875         public static final String COLUMN_REMAINING_BACKGROUND_WRITE_BYTES =
876                 "remaining_background_write_bytes";
877         public static final String COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES =
878                 "remaining_garage_mode_write_bytes";
879         public static final String COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES =
880                 "forgiven_foreground_write_bytes";
881         public static final String COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES =
882                 "forgiven_background_write_bytes";
883         public static final String COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES =
884                 "forgiven_garage_mode_write_bytes";
885 
createTable(SQLiteDatabase db)886         public static void createTable(SQLiteDatabase db) {
887             StringBuilder createCommand = new StringBuilder();
888             createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (")
889                     .append(COLUMN_USER_PACKAGE_ID).append(" INTEGER NOT NULL, ")
890                     .append(COLUMN_DATE_EPOCH).append(" INTEGER NOT NULL, ")
891                     .append(COLUMN_NUM_OVERUSES).append(" INTEGER NOT NULL, ")
892                     .append(COLUMN_NUM_FORGIVEN_OVERUSES).append(" INTEGER NOT NULL, ")
893                     .append(COLUMN_NUM_TIMES_KILLED).append(" INTEGER NOT NULL, ")
894                     .append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" INTEGER, ")
895                     .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" INTEGER, ")
896                     .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(" INTEGER, ")
897                     .append(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES).append(" INTEGER, ")
898                     .append(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES).append(" INTEGER, ")
899                     .append(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES).append(" INTEGER, ")
900                     .append(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES).append(" INTEGER, ")
901                     .append(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES).append(" INTEGER, ")
902                     .append(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES).append(" INTEGER, ")
903                     .append("PRIMARY KEY (").append(COLUMN_USER_PACKAGE_ID).append(", ")
904                     .append(COLUMN_DATE_EPOCH).append("), FOREIGN KEY (")
905                     .append(COLUMN_USER_PACKAGE_ID).append(") REFERENCES ")
906                     .append(UserPackageSettingsTable.TABLE_NAME).append(" (")
907                     .append(UserPackageSettingsTable.COLUMN_USER_PACKAGE_ID)
908                     .append(") ON DELETE CASCADE)");
909             db.execSQL(createCommand.toString());
910             Slogf.i(TAG, "Successfully created the %s table in the %s database version %d",
911                     TABLE_NAME, WatchdogDbHelper.DATABASE_NAME, WatchdogDbHelper.DATABASE_VERSION);
912         }
913 
getContentValues( String userPackageId, IoUsageStatsEntry entry, long statsDateEpochSeconds)914         public static ContentValues getContentValues(
915                 String userPackageId, IoUsageStatsEntry entry, long statsDateEpochSeconds) {
916             android.automotive.watchdog.IoOveruseStats ioOveruseStats =
917                     entry.ioUsage.getInternalIoOveruseStats();
918             ContentValues values = new ContentValues();
919             values.put(COLUMN_USER_PACKAGE_ID, userPackageId);
920             values.put(COLUMN_DATE_EPOCH, statsDateEpochSeconds);
921             values.put(COLUMN_NUM_OVERUSES, ioOveruseStats.totalOveruses);
922             values.put(COLUMN_NUM_FORGIVEN_OVERUSES, entry.ioUsage.getForgivenOveruses());
923             values.put(COLUMN_NUM_TIMES_KILLED, entry.ioUsage.getTotalTimesKilled());
924             values.put(
925                     COLUMN_WRITTEN_FOREGROUND_BYTES, ioOveruseStats.writtenBytes.foregroundBytes);
926             values.put(
927                     COLUMN_WRITTEN_BACKGROUND_BYTES, ioOveruseStats.writtenBytes.backgroundBytes);
928             values.put(
929                     COLUMN_WRITTEN_GARAGE_MODE_BYTES, ioOveruseStats.writtenBytes.garageModeBytes);
930             values.put(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES,
931                     ioOveruseStats.remainingWriteBytes.foregroundBytes);
932             values.put(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES,
933                     ioOveruseStats.remainingWriteBytes.backgroundBytes);
934             values.put(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES,
935                     ioOveruseStats.remainingWriteBytes.garageModeBytes);
936             android.automotive.watchdog.PerStateBytes forgivenWriteBytes =
937                     entry.ioUsage.getForgivenWriteBytes();
938             values.put(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES, forgivenWriteBytes.foregroundBytes);
939             values.put(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES, forgivenWriteBytes.backgroundBytes);
940             values.put(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES, forgivenWriteBytes.garageModeBytes);
941             return values;
942         }
943 
queryStats( SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds)944         public static ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> queryStats(
945                 SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds) {
946             StringBuilder queryBuilder = new StringBuilder();
947             queryBuilder.append("SELECT ")
948                     .append(COLUMN_USER_PACKAGE_ID).append(", ")
949                     .append("MIN(").append(COLUMN_DATE_EPOCH).append("), ")
950                     .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
951                     .append("SUM(").append(COLUMN_NUM_FORGIVEN_OVERUSES).append("), ")
952                     .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ")
953                     .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
954                     .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
955                     .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
956                     .append("SUM(").append(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES).append("), ")
957                     .append("SUM(").append(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES).append("), ")
958                     .append("SUM(").append(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES).append("), ")
959                     .append("SUM(").append(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES).append("), ")
960                     .append("SUM(").append(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES).append("), ")
961                     .append("SUM(").append(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES).append(") ")
962                     .append("FROM ").append(TABLE_NAME).append(" WHERE ")
963                     .append(COLUMN_DATE_EPOCH).append(">= ? and ")
964                     .append(COLUMN_DATE_EPOCH).append("< ? GROUP BY ")
965                     .append(COLUMN_USER_PACKAGE_ID);
966             String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds),
967                     String.valueOf(excludingEndEpochSeconds)};
968 
969             ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsageById = new ArrayMap<>();
970             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
971                 while (cursor.moveToNext()) {
972                     android.automotive.watchdog.IoOveruseStats ioOveruseStats =
973                             new android.automotive.watchdog.IoOveruseStats();
974                     ioOveruseStats.startTime = cursor.getLong(1);
975                     ioOveruseStats.durationInSeconds =
976                             excludingEndEpochSeconds - includingStartEpochSeconds;
977                     ioOveruseStats.totalOveruses = cursor.getInt(2);
978                     ioOveruseStats.writtenBytes = new PerStateBytes();
979                     ioOveruseStats.writtenBytes.foregroundBytes = cursor.getLong(5);
980                     ioOveruseStats.writtenBytes.backgroundBytes = cursor.getLong(6);
981                     ioOveruseStats.writtenBytes.garageModeBytes = cursor.getLong(7);
982                     ioOveruseStats.remainingWriteBytes = new PerStateBytes();
983                     ioOveruseStats.remainingWriteBytes.foregroundBytes = cursor.getLong(8);
984                     ioOveruseStats.remainingWriteBytes.backgroundBytes = cursor.getLong(9);
985                     ioOveruseStats.remainingWriteBytes.garageModeBytes = cursor.getLong(10);
986                     PerStateBytes forgivenWriteBytes = new PerStateBytes();
987                     forgivenWriteBytes.foregroundBytes = cursor.getLong(11);
988                     forgivenWriteBytes.backgroundBytes = cursor.getLong(12);
989                     forgivenWriteBytes.garageModeBytes = cursor.getLong(13);
990 
991                     ioUsageById.put(cursor.getString(0), new WatchdogPerfHandler.PackageIoUsage(
992                             ioOveruseStats, forgivenWriteBytes,
993                             /* forgivenOveruses= */ cursor.getInt(3),
994                             /* totalTimesKilled= */ cursor.getInt(4)));
995                 }
996             }
997             return ioUsageById;
998         }
999 
queryIoOveruseStatsForUserPackageId( SQLiteDatabase db, String userPackageId, long includingStartEpochSeconds, long excludingEndEpochSeconds)1000         public static @Nullable IoOveruseStats queryIoOveruseStatsForUserPackageId(
1001                 SQLiteDatabase db, String userPackageId, long includingStartEpochSeconds,
1002                 long excludingEndEpochSeconds) {
1003             StringBuilder queryBuilder = new StringBuilder();
1004             queryBuilder.append("SELECT SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
1005                     .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ")
1006                     .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
1007                     .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
1008                     .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
1009                     .append("MIN(").append(COLUMN_DATE_EPOCH).append(") ")
1010                     .append("FROM ").append(TABLE_NAME).append(" WHERE ")
1011                     .append(COLUMN_USER_PACKAGE_ID).append("=? and ")
1012                     .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
1013                     .append(COLUMN_DATE_EPOCH).append("< ?");
1014             String[] selectionArgs = new String[]{userPackageId,
1015                     String.valueOf(includingStartEpochSeconds),
1016                     String.valueOf(excludingEndEpochSeconds)};
1017             long totalOveruses = 0;
1018             long totalTimesKilled = 0;
1019             long totalBytesWritten = 0;
1020             long earliestEpochSecond = excludingEndEpochSeconds;
1021             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
1022                 if (cursor.getCount() == 0) {
1023                     return null;
1024                 }
1025                 while (cursor.moveToNext()) {
1026                     totalOveruses += cursor.getLong(0);
1027                     totalTimesKilled += cursor.getLong(1);
1028                     totalBytesWritten += cursor.getLong(2) + cursor.getLong(3) + cursor.getLong(4);
1029                     earliestEpochSecond = Math.min(cursor.getLong(5), earliestEpochSecond);
1030                 }
1031             }
1032             if (totalBytesWritten == 0) {
1033                 return null;
1034             }
1035             long durationInSeconds = excludingEndEpochSeconds - earliestEpochSecond;
1036             IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder(
1037                     earliestEpochSecond, durationInSeconds);
1038             statsBuilder.setTotalOveruses(totalOveruses);
1039             statsBuilder.setTotalTimesKilled(totalTimesKilled);
1040             statsBuilder.setTotalBytesWritten(totalBytesWritten);
1041             return statsBuilder.build();
1042         }
1043 
queryNotForgivenHistoricalOveruses( SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds)1044         public static ArrayMap<String, Integer> queryNotForgivenHistoricalOveruses(
1045                 SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds) {
1046             StringBuilder queryBuilder = new StringBuilder("SELECT ")
1047                     .append(COLUMN_USER_PACKAGE_ID).append(", ")
1048                     .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
1049                     .append("SUM(").append(COLUMN_NUM_FORGIVEN_OVERUSES).append(") ")
1050                     .append("FROM ").append(TABLE_NAME).append(" WHERE ")
1051                     .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
1052                     .append(COLUMN_DATE_EPOCH).append("< ? GROUP BY ")
1053                     .append(COLUMN_USER_PACKAGE_ID);
1054             String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds),
1055                     String.valueOf(excludingEndEpochSeconds)};
1056             ArrayMap<String, Integer> notForgivenOverusesById = new ArrayMap<>();
1057             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
1058                 while (cursor.moveToNext()) {
1059                     if (cursor.getInt(1) <= cursor.getInt(2)) {
1060                         continue;
1061                     }
1062                     notForgivenOverusesById.put(cursor.getString(0),
1063                             cursor.getInt(1) - cursor.getInt(2));
1064                 }
1065             }
1066             return notForgivenOverusesById;
1067         }
1068 
forgiveHistoricalOverusesForPackage(SQLiteDatabase db, List<String> userPackageIds, long includingStartEpochSeconds, long excludingEndEpochSeconds)1069         public static void forgiveHistoricalOverusesForPackage(SQLiteDatabase db,
1070                 List<String> userPackageIds, long includingStartEpochSeconds,
1071                 long excludingEndEpochSeconds) {
1072             if (userPackageIds.isEmpty()) {
1073                 Slogf.e(TAG, "No user package ids provided to forgive historical overuses.");
1074                 return;
1075             }
1076             StringBuilder updateQueryBuilder = new StringBuilder("UPDATE ").append(TABLE_NAME)
1077                     .append(" SET ")
1078                     .append(COLUMN_NUM_FORGIVEN_OVERUSES).append("=").append(COLUMN_NUM_OVERUSES)
1079                     .append(" WHERE ")
1080                     .append(COLUMN_DATE_EPOCH).append(">= ").append(includingStartEpochSeconds)
1081                     .append(" and ")
1082                     .append(COLUMN_DATE_EPOCH).append("< ").append(excludingEndEpochSeconds);
1083             for (int i = 0; i < userPackageIds.size(); i++) {
1084                 if (i == 0) {
1085                     updateQueryBuilder.append(" and ").append(COLUMN_USER_PACKAGE_ID)
1086                             .append(" IN (");
1087                 } else {
1088                     updateQueryBuilder.append(", ");
1089                 }
1090                 updateQueryBuilder.append(userPackageIds.get(i));
1091                 if (i == userPackageIds.size() - 1) {
1092                     updateQueryBuilder.append(")");
1093                 }
1094             }
1095 
1096             db.execSQL(updateQueryBuilder.toString());
1097             Slogf.i(TAG, "Attempted to forgive overuses for I/O usage stats entries on pid %d",
1098                     Process.myPid());
1099         }
1100 
1101         public static @Nullable List<AtomsProto.CarWatchdogDailyIoUsageSummary>
queryDailySystemIoUsageSummaries(SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds)1102                 queryDailySystemIoUsageSummaries(SQLiteDatabase db, long includingStartEpochSeconds,
1103                 long excludingEndEpochSeconds) {
1104             StringBuilder queryBuilder = new StringBuilder();
1105             queryBuilder.append("SELECT SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
1106                     .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
1107                     .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
1108                     .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
1109                     .append("date(").append(COLUMN_DATE_EPOCH).append(", '").append(DATE_MODIFIER)
1110                     .append("', '").append(ZONE_MODIFIER).append("') as stats_date_epoch ")
1111                     .append("FROM ").append(TABLE_NAME).append(" WHERE ")
1112                     .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
1113                     .append(COLUMN_DATE_EPOCH).append(" < ? ")
1114                     .append("GROUP BY stats_date_epoch ")
1115                     .append("HAVING SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" + ")
1116                     .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" + ")
1117                     .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(") > 0 ")
1118                     .append("ORDER BY stats_date_epoch ASC");
1119 
1120             String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds),
1121                     String.valueOf(excludingEndEpochSeconds)};
1122             List<AtomsProto.CarWatchdogDailyIoUsageSummary> summaries = new ArrayList<>();
1123             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
1124                 if (cursor.getCount() == 0) {
1125                     return null;
1126                 }
1127                 while (cursor.moveToNext()) {
1128                     summaries.add(AtomsProto.CarWatchdogDailyIoUsageSummary.newBuilder()
1129                             .setWrittenBytes(WatchdogPerfHandler.constructCarWatchdogPerStateBytes(
1130                                     /* foregroundBytes= */ cursor.getLong(1),
1131                                     /* backgroundBytes= */ cursor.getLong(2),
1132                                     /* garageModeBytes= */ cursor.getLong(3)))
1133                             .setOveruseCount(cursor.getInt(0))
1134                             .build());
1135                 }
1136             }
1137             return summaries;
1138         }
1139 
querySystemTotalWrittenBytes(SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds)1140         public static long querySystemTotalWrittenBytes(SQLiteDatabase db,
1141                 long includingStartEpochSeconds, long excludingEndEpochSeconds) {
1142             StringBuilder queryBuilder = new StringBuilder();
1143             queryBuilder.append("SELECT SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" + ")
1144                     .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" + ")
1145                     .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(") ")
1146                     .append("FROM ").append(TABLE_NAME).append(" WHERE ")
1147                     .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
1148                     .append(COLUMN_DATE_EPOCH).append(" < ? ");
1149 
1150             String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds),
1151                     String.valueOf(excludingEndEpochSeconds)};
1152             long totalWrittenBytes = 0;
1153             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
1154                 while (cursor.moveToNext()) {
1155                     totalWrittenBytes += cursor.getLong(0);
1156                 }
1157             }
1158             return totalWrittenBytes;
1159         }
1160 
1161         public static @Nullable ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>>
queryTopUsersDailyIoUsageSummaries(SQLiteDatabase db, int numTopUsers, long includingStartEpochSeconds, long excludingEndEpochSeconds)1162                 queryTopUsersDailyIoUsageSummaries(SQLiteDatabase db, int numTopUsers,
1163                 long includingStartEpochSeconds, long excludingEndEpochSeconds) {
1164             StringBuilder innerQueryBuilder = new StringBuilder();
1165             innerQueryBuilder.append("SELECT ").append(COLUMN_USER_PACKAGE_ID)
1166                     .append(" FROM (SELECT ").append(COLUMN_USER_PACKAGE_ID).append(", ")
1167                     .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" + ")
1168                     .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" + ")
1169                     .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(") AS total_written_bytes ")
1170                     .append("FROM ").append(TABLE_NAME).append(" WHERE ")
1171                     .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
1172                     .append(COLUMN_DATE_EPOCH).append(" < ?")
1173                     .append(" GROUP BY ").append(COLUMN_USER_PACKAGE_ID)
1174                     .append(" ORDER BY total_written_bytes DESC LIMIT ").append(numTopUsers)
1175                     .append(')');
1176 
1177             StringBuilder queryBuilder = new StringBuilder();
1178             queryBuilder.append("SELECT ").append(COLUMN_USER_PACKAGE_ID).append(", ")
1179                     .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
1180                     .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
1181                     .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
1182                     .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
1183                     .append("date(").append(COLUMN_DATE_EPOCH).append(", '").append(DATE_MODIFIER)
1184                     .append("', '").append(ZONE_MODIFIER).append("') as stats_date_epoch ")
1185                     .append("FROM ").append(TABLE_NAME).append(" WHERE ")
1186                     .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
1187                     .append(COLUMN_DATE_EPOCH).append(" < ? and (")
1188                     .append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" > 0 or ")
1189                     .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" > 0 or ")
1190                     .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(" > 0) and ")
1191                     .append(COLUMN_USER_PACKAGE_ID)
1192                     .append(" in (").append(innerQueryBuilder)
1193                     .append(") GROUP BY stats_date_epoch, ").append(COLUMN_USER_PACKAGE_ID)
1194                     .append(" ORDER BY ").append(COLUMN_USER_PACKAGE_ID)
1195                     .append(", stats_date_epoch ASC");
1196 
1197             String[] selectionArgs = new String[]{
1198                     // Outer query selection arguments.
1199                     String.valueOf(includingStartEpochSeconds),
1200                     String.valueOf(excludingEndEpochSeconds),
1201                     // Inner query selection arguments.
1202                     String.valueOf(includingStartEpochSeconds),
1203                     String.valueOf(excludingEndEpochSeconds)};
1204 
1205             ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>> summariesById =
1206                     new ArrayMap<>();
1207             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
1208                 if (cursor.getCount() == 0) {
1209                     return null;
1210                 }
1211                 while (cursor.moveToNext()) {
1212                     String id = cursor.getString(0);
1213                     List<AtomsProto.CarWatchdogDailyIoUsageSummary> summaries =
1214                             summariesById.get(id);
1215                     if (summaries == null) {
1216                         summaries = new ArrayList<>();
1217                     }
1218                     summaries.add(AtomsProto.CarWatchdogDailyIoUsageSummary.newBuilder()
1219                             .setWrittenBytes(WatchdogPerfHandler.constructCarWatchdogPerStateBytes(
1220                                     /* foregroundBytes= */ cursor.getLong(2),
1221                                     /* backgroundBytes= */ cursor.getLong(3),
1222                                     /* garageModeBytes= */ cursor.getLong(4)))
1223                             .setOveruseCount(cursor.getInt(1))
1224                             .build());
1225                     summariesById.put(id, summaries);
1226                 }
1227             }
1228             return summariesById;
1229         }
1230 
truncateToDate(SQLiteDatabase db, ZonedDateTime latestTruncateDate)1231         public static void truncateToDate(SQLiteDatabase db, ZonedDateTime latestTruncateDate) {
1232             String selection = COLUMN_DATE_EPOCH + " <= ?";
1233             String[] selectionArgs = { String.valueOf(latestTruncateDate.toEpochSecond()) };
1234 
1235             int rows = db.delete(TABLE_NAME, selection, selectionArgs);
1236             Slogf.i(TAG, "Truncated %d I/O usage stats entries on pid %d", rows, Process.myPid());
1237         }
1238 
trimHistoricalStats(SQLiteDatabase db, ZonedDateTime currentDate)1239         public static void trimHistoricalStats(SQLiteDatabase db, ZonedDateTime currentDate) {
1240             ContentValues values = new ContentValues();
1241             values.putNull(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES);
1242             values.putNull(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES);
1243             values.putNull(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES);
1244             values.putNull(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES);
1245             values.putNull(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES);
1246             values.putNull(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES);
1247 
1248             String selection = COLUMN_DATE_EPOCH + " < ?";
1249             String[] selectionArgs = { String.valueOf(currentDate.toEpochSecond()) };
1250 
1251             int rows = db.update(TABLE_NAME, values, selection, selectionArgs);
1252             Slogf.i(TAG, "Trimmed %d I/O usage stats entries on pid %d", rows, Process.myPid());
1253         }
1254     }
1255 
1256     /**
1257      * Defines the Watchdog database and database level operations.
1258      */
1259     static final class WatchdogDbHelper extends SQLiteOpenHelper {
1260         public static final String DATABASE_NAME = "car_watchdog.db";
1261 
1262         private static final int DATABASE_VERSION = 2;
1263 
1264         private ZonedDateTime mLatestShrinkDate;
1265         private TimeSource mTimeSource;
1266 
WatchdogDbHelper(Context context, boolean useDataSystemCarDir, TimeSource timeSource)1267         WatchdogDbHelper(Context context, boolean useDataSystemCarDir, TimeSource timeSource) {
1268             /* Use device protected storage because CarService may need to access the database
1269              * before the user has authenticated.
1270              */
1271             super(context.createDeviceProtectedStorageContext(), useDataSystemCarDir
1272                             ? new File(CarWatchdogService.getWatchdogDirFile(), DATABASE_NAME)
1273                                     .getAbsolutePath()
1274                             : DATABASE_NAME,
1275                     /* name= */ null, DATABASE_VERSION);
1276             mTimeSource = timeSource;
1277         }
1278 
1279         @Override
onCreate(SQLiteDatabase db)1280         public void onCreate(SQLiteDatabase db) {
1281             UserPackageSettingsTable.createTable(db);
1282             IoUsageStatsTable.createTable(db);
1283         }
1284 
1285         @Override
onConfigure(SQLiteDatabase db)1286         public void onConfigure(SQLiteDatabase db) {
1287             db.setForeignKeyConstraintsEnabled(true);
1288         }
1289 
close()1290         public synchronized void close() {
1291             super.close();
1292             mLatestShrinkDate = null;
1293         }
1294 
onShrink(SQLiteDatabase db)1295         public void onShrink(SQLiteDatabase db) {
1296             ZonedDateTime currentDate = mTimeSource.getCurrentDate();
1297             if (currentDate.equals(mLatestShrinkDate)) {
1298                 return;
1299             }
1300             IoUsageStatsTable.truncateToDate(db, currentDate.minus(RETENTION_PERIOD));
1301             IoUsageStatsTable.trimHistoricalStats(db, currentDate);
1302             mLatestShrinkDate = currentDate;
1303             Slogf.i(TAG, "Shrunk watchdog database for the date '%s'", mLatestShrinkDate);
1304         }
1305 
1306         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion)1307         public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
1308             if (oldVersion != 1) {
1309                 return;
1310             }
1311             // Upgrade logic from version 1 to 2.
1312             int upgradeVersion = oldVersion;
1313             db.beginTransaction();
1314             try {
1315                 upgradeToVersion2(db);
1316                 db.setTransactionSuccessful();
1317                 upgradeVersion = currentVersion;
1318                 Slogf.i(TAG, "Successfully upgraded database from version %d to %d", oldVersion,
1319                         upgradeVersion);
1320             } finally {
1321                 db.endTransaction();
1322             }
1323             if (upgradeVersion != currentVersion) {
1324                 Slogf.i(TAG, "Failed to upgrade database from version %d to %d. "
1325                         + "Attempting to recreate database.", oldVersion, currentVersion);
1326                 recreateDatabase(db);
1327             }
1328         }
1329 
1330         /**
1331          * Upgrades the given {@code db} to version 2.
1332          *
1333          * <p>Database version 2 replaces the primary key in {@link UserPackageSettingsTable} with
1334          * an auto-incrementing integer ID and uses the ID (instead of its rowid) as one of
1335          * the primary keys in {@link IoUsageStatsTable} along with a foreign key dependency.
1336          *
1337          * <p>Only the entries from {@link UserPackageSettingsTable} are migrated to the version 2
1338          * database because in version 1 only the current day's entries in {@link IoUsageStatsTable}
1339          * are mappable to the former table and dropping these entries is tolerable.
1340          */
upgradeToVersion2(SQLiteDatabase db)1341         private void upgradeToVersion2(SQLiteDatabase db) {
1342             String oldUserPackageSettingsTable = UserPackageSettingsTable.TABLE_NAME + "_old_v1";
1343             StringBuilder execSql = new StringBuilder("ALTER TABLE ")
1344                     .append(UserPackageSettingsTable.TABLE_NAME)
1345                     .append(" RENAME TO ").append(oldUserPackageSettingsTable);
1346             db.execSQL(execSql.toString());
1347 
1348             execSql = new StringBuilder("DROP TABLE IF EXISTS ")
1349                     .append(IoUsageStatsTable.TABLE_NAME);
1350             db.execSQL(execSql.toString());
1351 
1352             UserPackageSettingsTable.createTable(db);
1353             IoUsageStatsTable.createTable(db);
1354 
1355             execSql = new StringBuilder("INSERT INTO ").append(UserPackageSettingsTable.TABLE_NAME)
1356                     .append(" (").append(UserPackageSettingsTable.COLUMN_PACKAGE_NAME).append(", ")
1357                     .append(UserPackageSettingsTable.COLUMN_USER_ID).append(", ")
1358                     .append(UserPackageSettingsTable.COLUMN_KILLABLE_STATE).append(") ")
1359                     .append("SELECT ").append(UserPackageSettingsTable.COLUMN_PACKAGE_NAME)
1360                     .append(", ").append(UserPackageSettingsTable.COLUMN_USER_ID).append(", ")
1361                     .append(UserPackageSettingsTable.COLUMN_KILLABLE_STATE).append(" FROM ")
1362                     .append(oldUserPackageSettingsTable);
1363             db.execSQL(execSql.toString());
1364 
1365             execSql = new StringBuilder("DROP TABLE IF EXISTS ")
1366                     .append(oldUserPackageSettingsTable);
1367             db.execSQL(execSql.toString());
1368         }
1369 
recreateDatabase(SQLiteDatabase db)1370         private void recreateDatabase(SQLiteDatabase db) {
1371             db.execSQL(new StringBuilder("DROP TABLE IF EXISTS ")
1372                     .append(UserPackageSettingsTable.TABLE_NAME).toString());
1373             db.execSQL(new StringBuilder("DROP TABLE IF EXISTS ")
1374                     .append(IoUsageStatsTable.TABLE_NAME).toString());
1375 
1376             onCreate(db);
1377             Slogf.e(TAG, "Successfully recreated database version %d", DATABASE_VERSION);
1378         }
1379     }
1380 
1381 
1382     private static final class UserPackage {
1383         public final String userPackageId;
1384         public final @UserIdInt int userId;
1385         public final String packageName;
1386 
UserPackage(String userPackageId, @UserIdInt int userId, String packageName)1387         UserPackage(String userPackageId, @UserIdInt int userId, String packageName) {
1388             this.userPackageId = userPackageId;
1389             this.userId = userId;
1390             this.packageName = packageName;
1391         }
1392 
getKey()1393         public String getKey() {
1394             return getKey(userId, packageName);
1395         }
1396 
getKey(int userId, String packageName)1397         public static String getKey(int userId, String packageName) {
1398             return String.format(Locale.ENGLISH, "%d:%s", userId, packageName);
1399         }
1400 
1401         @Override
toString()1402         public String toString() {
1403             return new StringBuilder("UserPackage{userPackageId: ").append(userPackageId)
1404                     .append(", userId: ").append(userId)
1405                     .append(", packageName: ").append(packageName).append("}").toString();
1406         }
1407     }
1408 }
1409