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