Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ac40e111 authored by Manjeet Rulhania's avatar Manjeet Rulhania Committed by Android (Google) Code Review
Browse files

Merge "Add sqlite implementation for app op history" into main

parents 8cb331c7 0d167b17
Loading
Loading
Loading
Loading
+57 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.appop;

/**
 * Represents an aggregated access event in the app-op history. An object of this class
 * represents one row/record in sqlite database table i.e. {@link AppOpHistoryTable}
 * All parameters except {@link #totalAccessCount}, {@link #totalRejectCount},
 * {@link  #totalDurationMillis} works as a key for the aggregation.
 *
 * @param uid                 The UID of the application that performed the operation.
 * @param packageName         The package name of the application.
 * @param opCode              The specific operation code (e.g., camera access, location access).
 * @param deviceId            The identifier of the device associated with the access.
 * @param attributionTag      The attribution tag associated with the access, if any. Can be null.
 * @param opFlags             Additional flags associated with the operation code.
 * @param uidState            The state of the UID at the time of access (e.g., foreground,
 *                            background).
 * @param attributionFlags    Flags related to the attribution.
 * @param attributionChainId  An ID to link related attribution events.
 * @param accessTimeMillis    The actual access time of first event in a time window.
 * @param durationMillis      The actual duration of first event in a time window.
 * @param totalDurationMillis Sum of app op access duration in a time window.
 * @param totalAccessCount    Sum of app op access counts in a time window.
 * @param totalRejectCount    Sum of app op reject counts in a time window.
 */
public record AggregatedAppOpAccessEvent(
        int uid,
        String packageName,
        int opCode,
        String deviceId,
        String attributionTag,
        int opFlags,
        int uidState,
        int attributionFlags,
        long attributionChainId,
        long accessTimeMillis,
        long durationMillis,
        long totalDurationMillis,
        int totalAccessCount,
        int totalRejectCount
) {
}
+272 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.appop;

import static com.android.server.appop.HistoricalRegistry.AggregationTimeWindow;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.content.Context;
import android.database.DatabaseErrorHandler;
import android.database.DefaultDatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteRawStatement;
import android.util.IntArray;
import android.util.Slog;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * Sqlite database helper to read/write app op events.
 */
class AppOpHistoryDbHelper extends SQLiteOpenHelper {
    private static final String LOG_TAG = "AppOpHistoryDbHelper";
    private final File mDatabaseFile;
    private static final boolean DEBUG = false;
    private final AggregationTimeWindow mAggregationTimeWindow;

    AppOpHistoryDbHelper(@NonNull Context context, @NonNull File databaseFile,
            AggregationTimeWindow aggregationTimeWindow, int databaseVersion) {
        super(context, databaseFile.getAbsolutePath(), null, databaseVersion,
                new AppOpHistoryDatabaseErrorHandler());
        mDatabaseFile = databaseFile;
        mAggregationTimeWindow = aggregationTimeWindow;
        setOpenParams(getDatabaseOpenParams());
    }

    private static SQLiteDatabase.OpenParams getDatabaseOpenParams() {
        return new SQLiteDatabase.OpenParams.Builder()
                .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
                .build();
    }

    @Override
    public void onConfigure(SQLiteDatabase db) {
        db.execSQL("PRAGMA synchronous = NORMAL");
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(AppOpHistoryTable.CREATE_TABLE_SQL);
        // Create index for discrete ops only, as they are read often for privacy dashboard.
        if (mAggregationTimeWindow == AggregationTimeWindow.SHORT) {
            db.execSQL(AppOpHistoryTable.CREATE_INDEX_SQL);
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }

    void insertAppOpHistory(@NonNull List<AggregatedAppOpAccessEvent> appOpEvents) {
        if (appOpEvents.isEmpty()) {
            return;
        }

        try {
            SQLiteDatabase db = getWritableDatabase();
            db.beginTransaction();
            try (SQLiteRawStatement statement = db.createRawStatement(
                    AppOpHistoryTable.INSERT_TABLE_SQL)) {
                for (AggregatedAppOpAccessEvent event : appOpEvents) {
                    try {
                        statement.bindInt(AppOpHistoryTable.UID_INDEX, event.uid());
                        bindTextOrNull(statement, AppOpHistoryTable.PACKAGE_NAME_INDEX,
                                event.packageName());
                        bindTextOrNull(statement, AppOpHistoryTable.DEVICE_ID_INDEX,
                                event.deviceId());
                        statement.bindInt(AppOpHistoryTable.OP_CODE_INDEX, event.opCode());
                        bindTextOrNull(statement, AppOpHistoryTable.ATTRIBUTION_TAG_INDEX,
                                event.attributionTag());
                        statement.bindInt(AppOpHistoryTable.UID_STATE_INDEX,
                                event.uidState());
                        statement.bindInt(AppOpHistoryTable.OP_FLAGS_INDEX,
                                event.opFlags());
                        statement.bindLong(AppOpHistoryTable.ATTRIBUTION_FLAGS_INDEX,
                                event.attributionFlags());
                        statement.bindLong(AppOpHistoryTable.CHAIN_ID_INDEX,
                                event.attributionChainId());
                        statement.bindLong(AppOpHistoryTable.ACCESS_TIME_INDEX,
                                event.accessTimeMillis());
                        statement.bindLong(
                                AppOpHistoryTable.DURATION_INDEX, event.durationMillis());
                        statement.bindLong(
                                AppOpHistoryTable.TOTAL_DURATION_INDEX,
                                event.totalDurationMillis());
                        statement.bindInt(AppOpHistoryTable.ACCESS_COUNT_INDEX,
                                event.totalAccessCount());
                        statement.bindLong(AppOpHistoryTable.REJECT_COUNT_INDEX,
                                event.totalRejectCount());
                        statement.step();
                    } catch (Exception exception) {
                        Slog.e(LOG_TAG, "Couldn't insert app op event: " + event, exception);
                    } finally {
                        statement.reset();
                    }
                }
                db.setTransactionSuccessful();
            } finally {
                try {
                    db.endTransaction();
                } catch (SQLiteException exception) {
                    Slog.e(LOG_TAG, "Couldn't commit transaction when inserting app ops, database"
                            + " file size (bytes) : " + mDatabaseFile.length(), exception);
                }
            }
        } catch (Exception ex) {
            Slog.e(LOG_TAG, "Couldn't insert app op records in " + mDatabaseFile.getName(), ex);
        }
    }

    List<AggregatedAppOpAccessEvent> getAppOpHistory(
            @AppOpsManager.HistoricalOpsRequestFilter int requestFilters, long beginTime,
            long endTime, int uidFilter, @Nullable String packageNameFilter,
            @Nullable String attributionTagFilter, IntArray opCodeFilter, int opFlagsFilter,
            int limit, String orderByColumn, boolean ascending) {
        List<AggregatedAppOpAccessEvent> results = new ArrayList<>();
        List<AppOpHistoryQueryHelper.SQLCondition> conditions =
                AppOpHistoryQueryHelper.prepareConditions(
                        beginTime, endTime, requestFilters, uidFilter, packageNameFilter,
                        attributionTagFilter, opCodeFilter, opFlagsFilter);
        String sql = AppOpHistoryQueryHelper.buildSqlQuery(
                AppOpHistoryTable.SELECT_TABLE_DATA, conditions, orderByColumn, ascending, limit);

        try {
            SQLiteDatabase db = getReadableDatabase();
            db.beginTransactionReadOnly();
            try (SQLiteRawStatement statement = db.createRawStatement(sql)) {
                AppOpHistoryQueryHelper.bindValues(statement, conditions);

                while (statement.step()) {
                    results.add(readFromStatement(statement));
                }
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        } catch (Exception ex) {
            Slog.e(LOG_TAG, "Couldn't read app op records from " + mDatabaseFile.getName(), ex);
        }
        return results;
    }

    /**
     * This will be used as an offset for inserting new chain id in discrete ops table.
     */
    long getLargestAttributionChainId() {
        long chainId = 0;
        try {
            SQLiteDatabase db = getReadableDatabase();
            db.beginTransactionReadOnly();
            try (SQLiteRawStatement statement = db.createRawStatement(
                    AppOpHistoryTable.SELECT_MAX_ATTRIBUTION_CHAIN_ID)) {
                if (statement.step()) {
                    chainId = statement.getColumnLong(0);
                    if (chainId < 0) {
                        chainId = 0;
                    }
                }
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        } catch (SQLiteException exception) {
            Slog.e(LOG_TAG, "Error reading attribution chain id", exception);
        }
        return chainId;
    }

    List<AggregatedAppOpAccessEvent> getAppOpHistory() {
        List<AggregatedAppOpAccessEvent> results = new ArrayList<>();

        SQLiteDatabase db = getReadableDatabase();
        db.beginTransactionReadOnly();
        try (SQLiteRawStatement statement =
                     db.createRawStatement(AppOpHistoryTable.SELECT_TABLE_DATA)) {
            while (statement.step()) {
                AggregatedAppOpAccessEvent event = readFromStatement(statement);
                results.add(event);
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
        return results;
    }

    private void bindTextOrNull(SQLiteRawStatement statement, int index, @Nullable String text) {
        if (text == null) {
            statement.bindNull(index);
        } else {
            statement.bindText(index, text);
        }
    }

    void execSQL(@NonNull String sql) {
        execSQL(sql, null);
    }

    void execSQL(@NonNull String sql, Object[] bindArgs) {
        if (DEBUG) {
            Slog.i(LOG_TAG, "DB execSQL, sql: " + sql);
        }
        SQLiteDatabase db = getWritableDatabase();
        if (bindArgs == null) {
            db.execSQL(sql);
        } else {
            db.execSQL(sql, bindArgs);
        }
    }

    private AggregatedAppOpAccessEvent readFromStatement(SQLiteRawStatement statement) {
        int uid = statement.getColumnInt(0);
        String packageName = statement.getColumnText(1);
        String deviceId = statement.getColumnText(2);
        int opCode = statement.getColumnInt(3);
        String attributionTag = statement.getColumnText(4);
        int uidState = statement.getColumnInt(5);
        int opFlags = statement.getColumnInt(6);
        int attributionFlags = statement.getColumnInt(7);
        int attributionChainId = statement.getColumnInt(8);
        long accessTime = statement.getColumnLong(9);
        long duration = statement.getColumnLong(10);
        long totalDuration = statement.getColumnLong(11);
        int totalAccessCount = statement.getColumnInt(12);
        int totalRejectCount = statement.getColumnInt(13);

        return new AggregatedAppOpAccessEvent(uid,
                packageName, opCode, deviceId, attributionTag,
                opFlags, uidState, attributionFlags, attributionChainId, accessTime,
                duration, totalDuration, totalAccessCount, totalRejectCount);
    }

    static final class AppOpHistoryDatabaseErrorHandler implements DatabaseErrorHandler {
        private final DefaultDatabaseErrorHandler mDefaultDatabaseErrorHandler =
                new DefaultDatabaseErrorHandler();

        @Override
        public void onCorruption(SQLiteDatabase dbObj) {
            Slog.e(LOG_TAG, "app ops database " + dbObj.getPath() + " got corrupted.");
            mDefaultDatabaseErrorHandler.onCorruption(dbObj);
        }
    }
}
+763 −0

File added.

Preview size limit exceeded, changes collapsed.

+204 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.appop;

import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.database.sqlite.SQLiteRawStatement;
import android.util.IntArray;
import android.util.Slog;

import java.util.ArrayList;
import java.util.List;

/**
 * A helper class to construct SQL queries for retrieving app ops history data from the
 * SQLite database.
 */
final class AppOpHistoryQueryHelper {
    private static final String TAG = "AppOpHistoryQueryHelper";

    static IntArray getAppOpCodes(@AppOpsManager.HistoricalOpsRequestFilter int filter,
            @Nullable String[] opNamesFilter) {
        if ((filter & AppOpsManager.FILTER_BY_OP_NAMES) != 0) {
            IntArray opCodes = new IntArray(opNamesFilter.length);
            for (int i = 0; i < opNamesFilter.length; i++) {
                int op;
                try {
                    op = AppOpsManager.strOpToOp(opNamesFilter[i]);
                } catch (IllegalArgumentException ex) {
                    Slog.w(TAG, "Appop name `" + opNamesFilter[i] + "` is not recognized.");
                    continue;
                }
                opCodes.add(op);
            }
            return opCodes;
        }
        return null;
    }

    static void bindValues(SQLiteRawStatement statement, List<SQLCondition> conditions) {
        int size = conditions.size();
        for (int i = 0; i < size; i++) {
            AppOpHistoryQueryHelper.SQLCondition condition = conditions.get(i);
            if (HistoricalRegistry.DEBUG) {
                Slog.i(TAG, condition + ", binding value = " + condition.getFilterValue());
            }
            switch (condition.getColumnFilter()) {
                case PACKAGE_NAME, ATTR_TAG -> statement.bindText(i + 1,
                        condition.getFilterValue().toString());
                case UID, OP_CODE_EQUAL, OP_FLAGS -> statement.bindInt(i + 1,
                        Integer.parseInt(condition.getFilterValue().toString()));
                case BEGIN_TIME, END_TIME -> statement.bindLong(i + 1,
                        Long.parseLong(condition.getFilterValue().toString()));
                case OP_CODE_IN -> Slog.d(TAG, "No binding for In operator");
                default -> Slog.w(TAG, "unknown sql condition " + condition);
            }
        }
    }

    static String buildSqlQuery(String baseSql, List<SQLCondition> conditions,
            String orderByColumn, boolean ascending, int limit) {
        StringBuilder sql = new StringBuilder(baseSql);
        if (!conditions.isEmpty()) {
            sql.append(" WHERE ");
            int size = conditions.size();
            for (int i = 0; i < size; i++) {
                sql.append(conditions.get(i).toString());
                if (i < size - 1) {
                    sql.append(" AND ");
                }
            }
        }

        if (orderByColumn != null) {
            sql.append(" ORDER BY ").append(orderByColumn);
            sql.append(ascending ? " ASC " : " DESC ");
        }
        if (limit > 0) {
            sql.append(" LIMIT ").append(limit);
        }
        if (HistoricalRegistry.DEBUG) {
            Slog.i(TAG, "Sql query " + sql);
        }
        return sql.toString();
    }

    /**
     * Creates where conditions for package, uid, attribution tag and app op codes,
     * app op codes condition does not support argument binding.
     */
    static List<SQLCondition> prepareConditions(long beginTime, long endTime, int requestFilters,
            int uid, @Nullable String packageName, @Nullable String attributionTag,
            IntArray opCodes, int opFlags) {
        final List<SQLCondition> conditions = new ArrayList<>();

        if (beginTime > 0) {
            conditions.add(new SQLCondition(ColumnFilter.BEGIN_TIME, beginTime));
        }
        if (endTime > 0) {
            conditions.add(new SQLCondition(ColumnFilter.END_TIME, endTime));
        }
        if (opFlags != 0) {
            conditions.add(new SQLCondition(ColumnFilter.OP_FLAGS, opFlags));
        }

        if (requestFilters != 0) {
            if ((requestFilters & AppOpsManager.FILTER_BY_PACKAGE_NAME) != 0) {
                conditions.add(new SQLCondition(ColumnFilter.PACKAGE_NAME, packageName));
            }
            if ((requestFilters & AppOpsManager.FILTER_BY_UID) != 0) {
                conditions.add(new SQLCondition(ColumnFilter.UID, uid));

            }
            if ((requestFilters & AppOpsManager.FILTER_BY_ATTRIBUTION_TAG) != 0) {
                conditions.add(new SQLCondition(ColumnFilter.ATTR_TAG, attributionTag));
            }
            // filter op codes
            if (opCodes != null && opCodes.size() == 1) {
                conditions.add(new SQLCondition(ColumnFilter.OP_CODE_EQUAL, opCodes.get(0)));
            } else if (opCodes != null && opCodes.size() > 1) {
                StringBuilder b = new StringBuilder();
                int size = opCodes.size();
                for (int i = 0; i < size; i++) {
                    b.append(opCodes.get(i));
                    if (i < size - 1) {
                        b.append(", ");
                    }
                }
                conditions.add(new SQLCondition(ColumnFilter.OP_CODE_IN, b.toString()));
            }
        }
        return conditions;
    }

    /**
     * This class prepares a where clause condition for discrete ops table column.
     */
    static final class SQLCondition {
        private final ColumnFilter mColumnFilter;
        private final Object mFilterValue;

        SQLCondition(ColumnFilter columnFilter, Object filterValue) {
            mColumnFilter = columnFilter;
            mFilterValue = filterValue;
        }

        @Override
        public String toString() {
            if (mColumnFilter == ColumnFilter.OP_CODE_IN) {
                return mColumnFilter + " ( " + mFilterValue + " )";
            }
            return mColumnFilter.toString();
        }

        public ColumnFilter getColumnFilter() {
            return mColumnFilter;
        }

        public Object getFilterValue() {
            return mFilterValue;
        }
    }

    /**
     * This enum describes the where clause conditions for different columns in discrete ops
     * table.
     */
    enum ColumnFilter {
        PACKAGE_NAME(DiscreteOpsTable.Columns.PACKAGE_NAME + " = ? "),
        UID(DiscreteOpsTable.Columns.UID + " = ? "),
        ATTR_TAG(DiscreteOpsTable.Columns.ATTRIBUTION_TAG + " = ? "),
        END_TIME(DiscreteOpsTable.Columns.ACCESS_TIME + " <= ? "),
        OP_CODE_EQUAL(DiscreteOpsTable.Columns.OP_CODE + " = ? "),
        BEGIN_TIME(DiscreteOpsTable.Columns.ACCESS_TIME + " + "
                + DiscreteOpsTable.EFFECTIVE_ACCESS_DURATION + " >= ? "),
        OP_FLAGS("(" + DiscreteOpsTable.Columns.OP_FLAGS + " & ? ) != 0"),
        OP_CODE_IN(DiscreteOpsTable.Columns.OP_CODE + " IN ");

        final String mCondition;

        ColumnFilter(String condition) {
            mCondition = condition;
        }

        @Override
        public String toString() {
            return mCondition;
        }
    }
}
+173 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading