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