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