1 /* 2 * Copyright (C) 2024 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.server.appop; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.AppOpsManager; 22 import android.content.Context; 23 import android.database.DatabaseErrorHandler; 24 import android.database.DefaultDatabaseErrorHandler; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.database.sqlite.SQLiteException; 27 import android.database.sqlite.SQLiteFullException; 28 import android.database.sqlite.SQLiteOpenHelper; 29 import android.database.sqlite.SQLiteRawStatement; 30 import android.os.Environment; 31 import android.os.SystemClock; 32 import android.permission.flags.Flags; 33 import android.util.IntArray; 34 import android.util.Slog; 35 36 import com.android.internal.util.FrameworkStatsLog; 37 38 import java.io.File; 39 import java.util.ArrayList; 40 import java.util.List; 41 42 class DiscreteOpsDbHelper extends SQLiteOpenHelper { 43 private static final String LOG_TAG = "DiscreteOpsDbHelper"; 44 static final String DATABASE_NAME = "app_op_history.db"; 45 private static final int DATABASE_VERSION = 1; 46 private static final boolean DEBUG = false; 47 DiscreteOpsDbHelper(@onNull Context context, @NonNull File databaseFile)48 DiscreteOpsDbHelper(@NonNull Context context, @NonNull File databaseFile) { 49 super(context, databaseFile.getAbsolutePath(), null, DATABASE_VERSION, 50 new DiscreteOpsDatabaseErrorHandler()); 51 setOpenParams(getDatabaseOpenParams()); 52 } 53 getDatabaseOpenParams()54 private static SQLiteDatabase.OpenParams getDatabaseOpenParams() { 55 return new SQLiteDatabase.OpenParams.Builder() 56 .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) 57 .build(); 58 } 59 60 @NonNull getDatabaseFile()61 static File getDatabaseFile() { 62 return new File(new File(Environment.getDataSystemDirectory(), "appops"), DATABASE_NAME); 63 } 64 65 @Override onConfigure(SQLiteDatabase db)66 public void onConfigure(SQLiteDatabase db) { 67 db.execSQL("PRAGMA synchronous = NORMAL"); 68 } 69 70 @Override onCreate(SQLiteDatabase db)71 public void onCreate(SQLiteDatabase db) { 72 db.execSQL(DiscreteOpsTable.CREATE_TABLE_SQL); 73 db.execSQL(DiscreteOpsTable.CREATE_INDEX_SQL); 74 } 75 76 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)77 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 78 } 79 insertDiscreteOps(@onNull List<DiscreteOpsSqlRegistry.DiscreteOp> opEvents)80 void insertDiscreteOps(@NonNull List<DiscreteOpsSqlRegistry.DiscreteOp> opEvents) { 81 if (opEvents.isEmpty()) { 82 return; 83 } 84 long startTime = 0; 85 if (Flags.sqliteDiscreteOpEventLoggingEnabled()) { 86 startTime = SystemClock.elapsedRealtime(); 87 } 88 89 SQLiteDatabase db = getWritableDatabase(); 90 // TODO (b/383157289) what if database is busy and can't start a transaction? will read 91 // more about it and can be done in a follow up cl. 92 db.beginTransaction(); 93 try (SQLiteRawStatement statement = db.createRawStatement( 94 DiscreteOpsTable.INSERT_TABLE_SQL)) { 95 for (DiscreteOpsSqlRegistry.DiscreteOp event : opEvents) { 96 try { 97 statement.bindInt(DiscreteOpsTable.UID_INDEX, event.getUid()); 98 bindTextOrNull(statement, DiscreteOpsTable.PACKAGE_NAME_INDEX, 99 event.getPackageName()); 100 bindTextOrNull(statement, DiscreteOpsTable.DEVICE_ID_INDEX, 101 event.getDeviceId()); 102 statement.bindInt(DiscreteOpsTable.OP_CODE_INDEX, event.getOpCode()); 103 bindTextOrNull(statement, DiscreteOpsTable.ATTRIBUTION_TAG_INDEX, 104 event.getAttributionTag()); 105 statement.bindLong(DiscreteOpsTable.ACCESS_TIME_INDEX, event.getAccessTime()); 106 statement.bindLong( 107 DiscreteOpsTable.ACCESS_DURATION_INDEX, event.getDuration()); 108 statement.bindInt(DiscreteOpsTable.UID_STATE_INDEX, event.getUidState()); 109 statement.bindInt(DiscreteOpsTable.OP_FLAGS_INDEX, event.getOpFlags()); 110 statement.bindInt(DiscreteOpsTable.ATTRIBUTION_FLAGS_INDEX, 111 event.getAttributionFlags()); 112 statement.bindLong(DiscreteOpsTable.CHAIN_ID_INDEX, event.getChainId()); 113 statement.step(); 114 } catch (Exception exception) { 115 Slog.e(LOG_TAG, "Error inserting the discrete op: " + event, exception); 116 } finally { 117 statement.reset(); 118 } 119 } 120 db.setTransactionSuccessful(); 121 } finally { 122 try { 123 db.endTransaction(); 124 } catch (SQLiteException exception) { 125 Slog.e(LOG_TAG, "Couldn't commit transaction when inserting discrete ops, database" 126 + " file size (bytes) : " + getDatabaseFile().length(), exception); 127 } 128 } 129 if (Flags.sqliteDiscreteOpEventLoggingEnabled()) { 130 long timeTaken = SystemClock.elapsedRealtime() - startTime; 131 FrameworkStatsLog.write(FrameworkStatsLog.SQLITE_DISCRETE_OP_EVENT_REPORTED, 132 -1, timeTaken, getDatabaseFile().length()); 133 } 134 } 135 bindTextOrNull(SQLiteRawStatement statement, int index, @Nullable String text)136 private void bindTextOrNull(SQLiteRawStatement statement, int index, @Nullable String text) { 137 if (text == null) { 138 statement.bindNull(index); 139 } else { 140 statement.bindText(index, text); 141 } 142 } 143 144 /** 145 * This will be used as an offset for inserting new chain id in discrete ops table. 146 */ getLargestAttributionChainId()147 long getLargestAttributionChainId() { 148 long chainId = 0; 149 try { 150 SQLiteDatabase db = getReadableDatabase(); 151 db.beginTransactionReadOnly(); 152 try (SQLiteRawStatement statement = 153 db.createRawStatement(DiscreteOpsTable.SELECT_MAX_ATTRIBUTION_CHAIN_ID)) { 154 if (statement.step()) { 155 chainId = statement.getColumnLong(0); 156 if (chainId < 0) { 157 chainId = 0; 158 } 159 } 160 db.setTransactionSuccessful(); 161 } finally { 162 db.endTransaction(); 163 } 164 } catch (SQLiteException exception) { 165 Slog.e(LOG_TAG, "Error reading attribution chain id", exception); 166 } 167 return chainId; 168 } 169 execSQL(@onNull String sql)170 void execSQL(@NonNull String sql) { 171 execSQL(sql, null); 172 } 173 execSQL(@onNull String sql, Object[] bindArgs)174 void execSQL(@NonNull String sql, Object[] bindArgs) { 175 if (DEBUG) { 176 Slog.i(LOG_TAG, "DB execSQL, sql: " + sql); 177 } 178 try { 179 SQLiteDatabase db = getWritableDatabase(); 180 if (bindArgs == null) { 181 db.execSQL(sql); 182 } else { 183 db.execSQL(sql, bindArgs); 184 } 185 } catch (SQLiteFullException exception) { 186 Slog.e(LOG_TAG, "Couldn't exec sql command, disk is full. Discrete ops " 187 + "db file size (bytes) : " + getDatabaseFile().length(), exception); 188 } 189 } 190 191 /** 192 * Returns a list of {@link DiscreteOpsSqlRegistry.DiscreteOp} based on the given filters. 193 */ getDiscreteOps( @ppOpsManager.HistoricalOpsRequestFilter int requestFilters, int uidFilter, @Nullable String packageNameFilter, @Nullable String attributionTagFilter, IntArray opCodesFilter, int opFlagsFilter, long beginTime, long endTime, int limit, String orderByColumn, boolean ascending)194 List<DiscreteOpsSqlRegistry.DiscreteOp> getDiscreteOps( 195 @AppOpsManager.HistoricalOpsRequestFilter int requestFilters, 196 int uidFilter, @Nullable String packageNameFilter, 197 @Nullable String attributionTagFilter, IntArray opCodesFilter, int opFlagsFilter, 198 long beginTime, long endTime, int limit, String orderByColumn, boolean ascending) { 199 List<SQLCondition> conditions = prepareConditions(beginTime, endTime, requestFilters, 200 uidFilter, packageNameFilter, 201 attributionTagFilter, opCodesFilter, opFlagsFilter); 202 String sql = buildSql(conditions, orderByColumn, ascending, limit); 203 long startTime = 0; 204 if (Flags.sqliteDiscreteOpEventLoggingEnabled()) { 205 startTime = SystemClock.elapsedRealtime(); 206 } 207 SQLiteDatabase db = getReadableDatabase(); 208 List<DiscreteOpsSqlRegistry.DiscreteOp> results = new ArrayList<>(); 209 db.beginTransactionReadOnly(); 210 try (SQLiteRawStatement statement = db.createRawStatement(sql)) { 211 int size = conditions.size(); 212 for (int i = 0; i < size; i++) { 213 SQLCondition condition = conditions.get(i); 214 if (DEBUG) { 215 Slog.i(LOG_TAG, condition + ", binding value = " + condition.mFilterValue); 216 } 217 switch (condition.mColumnFilter) { 218 case PACKAGE_NAME, ATTR_TAG -> statement.bindText(i + 1, 219 condition.mFilterValue.toString()); 220 case UID, OP_CODE_EQUAL, OP_FLAGS -> statement.bindInt(i + 1, 221 Integer.parseInt(condition.mFilterValue.toString())); 222 case BEGIN_TIME, END_TIME -> statement.bindLong(i + 1, 223 Long.parseLong(condition.mFilterValue.toString())); 224 case OP_CODE_IN -> Slog.d(LOG_TAG, "No binding for In operator"); 225 default -> Slog.w(LOG_TAG, "unknown sql condition " + condition); 226 } 227 } 228 229 while (statement.step()) { 230 int uid = statement.getColumnInt(0); 231 String packageName = statement.getColumnText(1); 232 String deviceId = statement.getColumnText(2); 233 int opCode = statement.getColumnInt(3); 234 String attributionTag = statement.getColumnText(4); 235 long accessTime = statement.getColumnLong(5); 236 long duration = statement.getColumnLong(6); 237 int uidState = statement.getColumnInt(7); 238 int opFlags = statement.getColumnInt(8); 239 int attributionFlags = statement.getColumnInt(9); 240 long chainId = statement.getColumnLong(10); 241 DiscreteOpsSqlRegistry.DiscreteOp event = new DiscreteOpsSqlRegistry.DiscreteOp(uid, 242 packageName, attributionTag, deviceId, opCode, 243 opFlags, attributionFlags, uidState, chainId, accessTime, duration); 244 results.add(event); 245 } 246 db.setTransactionSuccessful(); 247 } finally { 248 db.endTransaction(); 249 } 250 if (Flags.sqliteDiscreteOpEventLoggingEnabled()) { 251 long timeTaken = SystemClock.elapsedRealtime() - startTime; 252 FrameworkStatsLog.write(FrameworkStatsLog.SQLITE_DISCRETE_OP_EVENT_REPORTED, 253 timeTaken, -1, getDatabaseFile().length()); 254 } 255 return results; 256 } 257 buildSql(List<SQLCondition> conditions, String orderByColumn, boolean ascending, int limit)258 private String buildSql(List<SQLCondition> conditions, String orderByColumn, boolean ascending, 259 int limit) { 260 StringBuilder sql = new StringBuilder(DiscreteOpsTable.SELECT_TABLE_DATA); 261 if (!conditions.isEmpty()) { 262 sql.append(" WHERE "); 263 int size = conditions.size(); 264 for (int i = 0; i < size; i++) { 265 sql.append(conditions.get(i).toString()); 266 if (i < size - 1) { 267 sql.append(" AND "); 268 } 269 } 270 } 271 272 if (orderByColumn != null) { 273 sql.append(" ORDER BY ").append(orderByColumn); 274 sql.append(ascending ? " ASC " : " DESC "); 275 } 276 if (limit > 0) { 277 sql.append(" LIMIT ").append(limit); 278 } 279 if (DEBUG) { 280 Slog.i(LOG_TAG, "Sql query " + sql); 281 } 282 return sql.toString(); 283 } 284 285 /** 286 * Creates where conditions for package, uid, attribution tag and app op codes, 287 * app op codes condition does not support argument binding. 288 */ prepareConditions(long beginTime, long endTime, int requestFilters, int uid, @Nullable String packageName, @Nullable String attributionTag, IntArray opCodes, int opFlags)289 private List<SQLCondition> prepareConditions(long beginTime, long endTime, int requestFilters, 290 int uid, @Nullable String packageName, @Nullable String attributionTag, 291 IntArray opCodes, int opFlags) { 292 final List<SQLCondition> conditions = new ArrayList<>(); 293 294 if (beginTime != -1) { 295 conditions.add(new SQLCondition(ColumnFilter.BEGIN_TIME, beginTime)); 296 } 297 if (endTime != -1) { 298 conditions.add(new SQLCondition(ColumnFilter.END_TIME, endTime)); 299 } 300 if (opFlags != 0) { 301 conditions.add(new SQLCondition(ColumnFilter.OP_FLAGS, opFlags)); 302 } 303 304 if (requestFilters != 0) { 305 if ((requestFilters & AppOpsManager.FILTER_BY_PACKAGE_NAME) != 0) { 306 conditions.add(new SQLCondition(ColumnFilter.PACKAGE_NAME, packageName)); 307 } 308 if ((requestFilters & AppOpsManager.FILTER_BY_UID) != 0) { 309 conditions.add(new SQLCondition(ColumnFilter.UID, uid)); 310 311 } 312 if ((requestFilters & AppOpsManager.FILTER_BY_ATTRIBUTION_TAG) != 0) { 313 conditions.add(new SQLCondition(ColumnFilter.ATTR_TAG, attributionTag)); 314 } 315 // filter op codes 316 if (opCodes != null && opCodes.size() == 1) { 317 conditions.add(new SQLCondition(ColumnFilter.OP_CODE_EQUAL, opCodes.get(0))); 318 } else if (opCodes != null && opCodes.size() > 1) { 319 StringBuilder b = new StringBuilder(); 320 int size = opCodes.size(); 321 for (int i = 0; i < size; i++) { 322 b.append(opCodes.get(i)); 323 if (i < size - 1) { 324 b.append(", "); 325 } 326 } 327 conditions.add(new SQLCondition(ColumnFilter.OP_CODE_IN, b.toString())); 328 } 329 } 330 return conditions; 331 } 332 333 /** 334 * This class prepares a where clause condition for discrete ops table column. 335 */ 336 static final class SQLCondition { 337 private final ColumnFilter mColumnFilter; 338 private final Object mFilterValue; 339 SQLCondition(ColumnFilter columnFilter, Object filterValue)340 SQLCondition(ColumnFilter columnFilter, Object filterValue) { 341 mColumnFilter = columnFilter; 342 mFilterValue = filterValue; 343 } 344 345 @Override toString()346 public String toString() { 347 if (mColumnFilter == ColumnFilter.OP_CODE_IN) { 348 return mColumnFilter + " ( " + mFilterValue + " )"; 349 } 350 return mColumnFilter.toString(); 351 } 352 } 353 354 /** 355 * This enum describes the where clause conditions for different columns in discrete ops 356 * table. 357 */ 358 private enum ColumnFilter { 359 PACKAGE_NAME(DiscreteOpsTable.Columns.PACKAGE_NAME + " = ? "), 360 UID(DiscreteOpsTable.Columns.UID + " = ? "), 361 ATTR_TAG(DiscreteOpsTable.Columns.ATTRIBUTION_TAG + " = ? "), 362 END_TIME(DiscreteOpsTable.Columns.ACCESS_TIME + " < ? "), 363 OP_CODE_EQUAL(DiscreteOpsTable.Columns.OP_CODE + " = ? "), 364 BEGIN_TIME(DiscreteOpsTable.Columns.ACCESS_TIME + " + " 365 + DiscreteOpsTable.Columns.ACCESS_DURATION + " > ? "), 366 OP_FLAGS("(" + DiscreteOpsTable.Columns.OP_FLAGS + " & ? ) != 0"), 367 OP_CODE_IN(DiscreteOpsTable.Columns.OP_CODE + " IN "); 368 369 final String mCondition; 370 ColumnFilter(String condition)371 ColumnFilter(String condition) { 372 mCondition = condition; 373 } 374 375 @Override toString()376 public String toString() { 377 return mCondition; 378 } 379 } 380 381 static final class DiscreteOpsDatabaseErrorHandler implements DatabaseErrorHandler { 382 private final DefaultDatabaseErrorHandler mDefaultDatabaseErrorHandler = 383 new DefaultDatabaseErrorHandler(); 384 385 @Override onCorruption(SQLiteDatabase dbObj)386 public void onCorruption(SQLiteDatabase dbObj) { 387 Slog.e(LOG_TAG, "discrete ops database got corrupted."); 388 mDefaultDatabaseErrorHandler.onCorruption(dbObj); 389 } 390 } 391 392 // USED for testing only getAllDiscreteOps(@onNull String sql)393 List<DiscreteOpsSqlRegistry.DiscreteOp> getAllDiscreteOps(@NonNull String sql) { 394 SQLiteDatabase db = getReadableDatabase(); 395 List<DiscreteOpsSqlRegistry.DiscreteOp> results = new ArrayList<>(); 396 db.beginTransactionReadOnly(); 397 try (SQLiteRawStatement statement = db.createRawStatement(sql)) { 398 while (statement.step()) { 399 int uid = statement.getColumnInt(0); 400 String packageName = statement.getColumnText(1); 401 String deviceId = statement.getColumnText(2); 402 int opCode = statement.getColumnInt(3); 403 String attributionTag = statement.getColumnText(4); 404 long accessTime = statement.getColumnLong(5); 405 long duration = statement.getColumnLong(6); 406 int uidState = statement.getColumnInt(7); 407 int opFlags = statement.getColumnInt(8); 408 int attributionFlags = statement.getColumnInt(9); 409 long chainId = statement.getColumnLong(10); 410 DiscreteOpsSqlRegistry.DiscreteOp event = new DiscreteOpsSqlRegistry.DiscreteOp(uid, 411 packageName, attributionTag, deviceId, opCode, 412 opFlags, attributionFlags, uidState, chainId, accessTime, duration); 413 results.add(event); 414 } 415 db.setTransactionSuccessful(); 416 } finally { 417 db.endTransaction(); 418 } 419 return results; 420 } 421 } 422