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