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  * <p>This manager may be accessed by multi-threads. All the database related methods are
51  * synchronized 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(
85             int uid, String packageName, int type, int anomalyState, long timestampMs) {
86         final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
87         ContentValues values = new ContentValues();
88         values.put(UID, uid);
89         values.put(PACKAGE_NAME, packageName);
90         values.put(ANOMALY_TYPE, type);
91         values.put(ANOMALY_STATE, anomalyState);
92         values.put(TIME_STAMP_MS, timestampMs);
93 
94         return db.insertWithOnConflict(TABLE_ANOMALY, null, values, CONFLICT_IGNORE) != -1;
95     }
96 
97     /**
98      * Query all the anomalies that happened after {@code timestampMsAfter} and with {@code state}.
99      */
queryAllAnomalies(long timestampMsAfter, int state)100     public synchronized List<AppInfo> queryAllAnomalies(long timestampMsAfter, int state) {
101         final List<AppInfo> appInfos = new ArrayList<>();
102         final SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
103         final String[] projection = {PACKAGE_NAME, ANOMALY_TYPE, UID};
104         final String orderBy = AnomalyDatabaseHelper.AnomalyColumns.TIME_STAMP_MS + " DESC";
105         final Map<Integer, AppInfo.Builder> mAppInfoBuilders = new ArrayMap<>();
106         final String selection = TIME_STAMP_MS + " > ? AND " + ANOMALY_STATE + " = ? ";
107         final String[] selectionArgs =
108                 new String[] {String.valueOf(timestampMsAfter), String.valueOf(state)};
109 
110         try (Cursor cursor =
111                 db.query(
112                         TABLE_ANOMALY,
113                         projection,
114                         selection,
115                         selectionArgs,
116                         null /* groupBy */,
117                         null /* having */,
118                         orderBy)) {
119             while (cursor.moveToNext()) {
120                 final int uid = cursor.getInt(cursor.getColumnIndex(UID));
121                 if (!mAppInfoBuilders.containsKey(uid)) {
122                     final AppInfo.Builder builder =
123                             new AppInfo.Builder()
124                                     .setUid(uid)
125                                     .setPackageName(
126                                             cursor.getString(cursor.getColumnIndex(PACKAGE_NAME)));
127                     mAppInfoBuilders.put(uid, builder);
128                 }
129                 mAppInfoBuilders
130                         .get(uid)
131                         .addAnomalyType(cursor.getInt(cursor.getColumnIndex(ANOMALY_TYPE)));
132             }
133         }
134 
135         for (Integer uid : mAppInfoBuilders.keySet()) {
136             appInfos.add(mAppInfoBuilders.get(uid).build());
137         }
138 
139         return appInfos;
140     }
141 
deleteAllAnomaliesBeforeTimeStamp(long timestampMs)142     public synchronized void deleteAllAnomaliesBeforeTimeStamp(long timestampMs) {
143         final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
144         db.delete(
145                 TABLE_ANOMALY, TIME_STAMP_MS + " < ?", new String[] {String.valueOf(timestampMs)});
146     }
147 
148     /**
149      * Update the type of anomalies to {@code state}
150      *
151      * @param appInfos represents the anomalies
152      * @param state which state to update to
153      */
updateAnomalies(List<AppInfo> appInfos, int state)154     public synchronized void updateAnomalies(List<AppInfo> appInfos, int state) {
155         if (!appInfos.isEmpty()) {
156             final int size = appInfos.size();
157             final String[] whereArgs = new String[size];
158             for (int i = 0; i < size; i++) {
159                 whereArgs[i] = appInfos.get(i).packageName;
160             }
161 
162             final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
163             final ContentValues values = new ContentValues();
164             values.put(ANOMALY_STATE, state);
165             db.update(
166                     TABLE_ANOMALY,
167                     values,
168                     PACKAGE_NAME
169                             + " IN ("
170                             + TextUtils.join(",", Collections.nCopies(appInfos.size(), "?"))
171                             + ")",
172                     whereArgs);
173         }
174     }
175 
176     /**
177      * Query latest timestamps when an app has been performed action {@code type}
178      *
179      * @param type of action been performed
180      * @return {@link SparseLongArray} where key is uid and value is timestamp
181      */
queryActionTime( @nomalyDatabaseHelper.ActionType int type)182     public synchronized SparseLongArray queryActionTime(
183             @AnomalyDatabaseHelper.ActionType int type) {
184         final SparseLongArray timeStamps = new SparseLongArray();
185         final SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
186         final String[] projection = {ActionColumns.UID, ActionColumns.TIME_STAMP_MS};
187         final String selection = ActionColumns.ACTION_TYPE + " = ? ";
188         final String[] selectionArgs = new String[] {String.valueOf(type)};
189 
190         try (Cursor cursor =
191                 db.query(
192                         TABLE_ACTION,
193                         projection,
194                         selection,
195                         selectionArgs,
196                         null /* groupBy */,
197                         null /* having */,
198                         null /* orderBy */)) {
199             final int uidIndex = cursor.getColumnIndex(ActionColumns.UID);
200             final int timestampIndex = cursor.getColumnIndex(ActionColumns.TIME_STAMP_MS);
201 
202             while (cursor.moveToNext()) {
203                 final int uid = cursor.getInt(uidIndex);
204                 final long timeStamp = cursor.getLong(timestampIndex);
205                 timeStamps.append(uid, timeStamp);
206             }
207         }
208 
209         return timeStamps;
210     }
211 
212     /** Insert an action, or update it if already existed */
insertAction( @nomalyDatabaseHelper.ActionType int type, int uid, String packageName, long timestampMs)213     public synchronized boolean insertAction(
214             @AnomalyDatabaseHelper.ActionType int type,
215             int uid,
216             String packageName,
217             long timestampMs) {
218         final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
219         final ContentValues values = new ContentValues();
220         values.put(ActionColumns.UID, uid);
221         values.put(ActionColumns.PACKAGE_NAME, packageName);
222         values.put(ActionColumns.ACTION_TYPE, type);
223         values.put(ActionColumns.TIME_STAMP_MS, timestampMs);
224 
225         return db.insertWithOnConflict(TABLE_ACTION, null, values, CONFLICT_REPLACE) != -1;
226     }
227 
228     /** Remove an action */
deleteAction( @nomalyDatabaseHelper.ActionType int type, int uid, String packageName)229     public synchronized boolean deleteAction(
230             @AnomalyDatabaseHelper.ActionType int type, int uid, String packageName) {
231         SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
232         final String where =
233                 ActionColumns.ACTION_TYPE
234                         + " = ? AND "
235                         + ActionColumns.UID
236                         + " = ? AND "
237                         + ActionColumns.PACKAGE_NAME
238                         + " = ? ";
239         final String[] whereArgs =
240                 new String[] {
241                     String.valueOf(type), String.valueOf(uid), String.valueOf(packageName)
242                 };
243 
244         return db.delete(TABLE_ACTION, where, whereArgs) != 0;
245     }
246 }
247