• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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