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