1 /*
2  * Copyright (C) 2018 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.settings.fuelgauge.batterytip;
18 
19 import static android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE;
20 import static android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE;
21 
22 import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.ANOMALY_STATE;
23 import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.ANOMALY_TYPE;
24 import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.PACKAGE_NAME;
25 import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.TIME_STAMP_MS;
26 import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.UID;
27 import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.Tables.TABLE_ACTION;
28 import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.Tables.TABLE_ANOMALY;
29 
30 import android.content.ContentValues;
31 import android.content.Context;
32 import android.database.Cursor;
33 import android.database.sqlite.SQLiteDatabase;
34 import android.text.TextUtils;
35 import android.util.ArrayMap;
36 import android.util.SparseLongArray;
37 
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.ActionColumns;
41 
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.List;
45 import java.util.Map;
46 
47 /**
48  * Database manager for battery data. Now it only contains anomaly data stored in {@link AppInfo}.
49  *
50  * This manager may be accessed by multi-threads. All the database related methods are synchronized
51  * so each operation won't be interfered by other threads.
52  */
53 public class BatteryDatabaseManager {
54     private static BatteryDatabaseManager sSingleton;
55 
56     private AnomalyDatabaseHelper mDatabaseHelper;
57 
BatteryDatabaseManager(Context context)58     private BatteryDatabaseManager(Context context) {
59         mDatabaseHelper = AnomalyDatabaseHelper.getInstance(context);
60     }
61 
getInstance(Context context)62     public static synchronized BatteryDatabaseManager getInstance(Context context) {
63         if (sSingleton == null) {
64             sSingleton = new BatteryDatabaseManager(context);
65         }
66         return sSingleton;
67     }
68 
69     @VisibleForTesting(otherwise = VisibleForTesting.NONE)
setUpForTest(BatteryDatabaseManager batteryDatabaseManager)70     public static void setUpForTest(BatteryDatabaseManager batteryDatabaseManager) {
71         sSingleton = batteryDatabaseManager;
72     }
73 
74     /**
75      * Insert an anomaly log to database.
76      *
77      * @param uid          the uid of the app
78      * @param packageName  the package name of the app
79      * @param type         the type of the anomaly
80      * @param anomalyState the state of the anomaly
81      * @param timestampMs  the time when it is happened
82      * @return {@code true} if insert operation succeed
83      */
insertAnomaly(int uid, String packageName, int type, int anomalyState, long timestampMs)84     public synchronized boolean insertAnomaly(int uid, String packageName, int type,
85             int anomalyState,
86             long timestampMs) {
87         final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
88         ContentValues values = new ContentValues();
89         values.put(UID, uid);
90         values.put(PACKAGE_NAME, packageName);
91         values.put(ANOMALY_TYPE, type);
92         values.put(ANOMALY_STATE, anomalyState);
93         values.put(TIME_STAMP_MS, timestampMs);
94 
95         return db.insertWithOnConflict(TABLE_ANOMALY, null, values, CONFLICT_IGNORE) != -1;
96     }
97 
98     /**
99      * Query all the anomalies that happened after {@code timestampMsAfter} and with {@code state}.
100      */
queryAllAnomalies(long timestampMsAfter, int state)101     public synchronized List<AppInfo> queryAllAnomalies(long timestampMsAfter, int state) {
102         final List<AppInfo> appInfos = new ArrayList<>();
103         final SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
104         final String[] projection = {PACKAGE_NAME, ANOMALY_TYPE, UID};
105         final String orderBy = AnomalyDatabaseHelper.AnomalyColumns.TIME_STAMP_MS + " DESC";
106         final Map<Integer, AppInfo.Builder> mAppInfoBuilders = new ArrayMap<>();
107         final String selection = TIME_STAMP_MS + " > ? AND " + ANOMALY_STATE + " = ? ";
108         final String[] selectionArgs = new String[]{String.valueOf(timestampMsAfter),
109                 String.valueOf(state)};
110 
111         try (Cursor cursor = db.query(TABLE_ANOMALY, projection, selection, selectionArgs,
112                 null /* groupBy */, null /* having */, orderBy)) {
113             while (cursor.moveToNext()) {
114                 final int uid = cursor.getInt(cursor.getColumnIndex(UID));
115                 if (!mAppInfoBuilders.containsKey(uid)) {
116                     final AppInfo.Builder builder = new AppInfo.Builder()
117                             .setUid(uid)
118                             .setPackageName(
119                                     cursor.getString(cursor.getColumnIndex(PACKAGE_NAME)));
120                     mAppInfoBuilders.put(uid, builder);
121                 }
122                 mAppInfoBuilders.get(uid).addAnomalyType(
123                         cursor.getInt(cursor.getColumnIndex(ANOMALY_TYPE)));
124             }
125         }
126 
127         for (Integer uid : mAppInfoBuilders.keySet()) {
128             appInfos.add(mAppInfoBuilders.get(uid).build());
129         }
130 
131         return appInfos;
132     }
133 
deleteAllAnomaliesBeforeTimeStamp(long timestampMs)134     public synchronized void deleteAllAnomaliesBeforeTimeStamp(long timestampMs) {
135         final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
136         db.delete(TABLE_ANOMALY, TIME_STAMP_MS + " < ?",
137                 new String[]{String.valueOf(timestampMs)});
138     }
139 
140     /**
141      * Update the type of anomalies to {@code state}
142      *
143      * @param appInfos represents the anomalies
144      * @param state    which state to update to
145      */
updateAnomalies(List<AppInfo> appInfos, int state)146     public synchronized void updateAnomalies(List<AppInfo> appInfos, int state) {
147         if (!appInfos.isEmpty()) {
148             final int size = appInfos.size();
149             final String[] whereArgs = new String[size];
150             for (int i = 0; i < size; i++) {
151                 whereArgs[i] = appInfos.get(i).packageName;
152             }
153 
154             final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
155             final ContentValues values = new ContentValues();
156             values.put(ANOMALY_STATE, state);
157             db.update(TABLE_ANOMALY, values, PACKAGE_NAME + " IN (" + TextUtils.join(",",
158                     Collections.nCopies(appInfos.size(), "?")) + ")", whereArgs);
159         }
160     }
161 
162     /**
163      * Query latest timestamps when an app has been performed action {@code type}
164      *
165      * @param type of action been performed
166      * @return {@link SparseLongArray} where key is uid and value is timestamp
167      */
queryActionTime( @nomalyDatabaseHelper.ActionType int type)168     public synchronized SparseLongArray queryActionTime(
169             @AnomalyDatabaseHelper.ActionType int type) {
170         final SparseLongArray timeStamps = new SparseLongArray();
171         final SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
172         final String[] projection = {ActionColumns.UID, ActionColumns.TIME_STAMP_MS};
173         final String selection = ActionColumns.ACTION_TYPE + " = ? ";
174         final String[] selectionArgs = new String[]{String.valueOf(type)};
175 
176         try (Cursor cursor = db.query(TABLE_ACTION, projection, selection, selectionArgs,
177                 null /* groupBy */, null /* having */, null /* orderBy */)) {
178             final int uidIndex = cursor.getColumnIndex(ActionColumns.UID);
179             final int timestampIndex = cursor.getColumnIndex(ActionColumns.TIME_STAMP_MS);
180 
181             while (cursor.moveToNext()) {
182                 final int uid = cursor.getInt(uidIndex);
183                 final long timeStamp = cursor.getLong(timestampIndex);
184                 timeStamps.append(uid, timeStamp);
185             }
186         }
187 
188         return timeStamps;
189     }
190 
191     /**
192      * Insert an action, or update it if already existed
193      */
insertAction(@nomalyDatabaseHelper.ActionType int type, int uid, String packageName, long timestampMs)194     public synchronized boolean insertAction(@AnomalyDatabaseHelper.ActionType int type,
195             int uid, String packageName, long timestampMs) {
196         final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
197         final ContentValues values = new ContentValues();
198         values.put(ActionColumns.UID, uid);
199         values.put(ActionColumns.PACKAGE_NAME, packageName);
200         values.put(ActionColumns.ACTION_TYPE, type);
201         values.put(ActionColumns.TIME_STAMP_MS, timestampMs);
202 
203         return db.insertWithOnConflict(TABLE_ACTION, null, values, CONFLICT_REPLACE) != -1;
204     }
205 
206     /**
207      * Remove an action
208      */
deleteAction(@nomalyDatabaseHelper.ActionType int type, int uid, String packageName)209     public synchronized boolean deleteAction(@AnomalyDatabaseHelper.ActionType int type,
210             int uid, String packageName) {
211         SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
212         final String where =
213                 ActionColumns.ACTION_TYPE + " = ? AND " + ActionColumns.UID + " = ? AND "
214                         + ActionColumns.PACKAGE_NAME + " = ? ";
215         final String[] whereArgs = new String[]{String.valueOf(type), String.valueOf(uid),
216                 String.valueOf(packageName)};
217 
218         return db.delete(TABLE_ACTION, where, whereArgs) != 0;
219     }
220 }
221