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

Commit c9de048c authored by MingWei Liao's avatar MingWei Liao Committed by Android (Google) Code Review
Browse files

Merge "Add AppFunctionAccessDatabaseHelper" into main

parents 8cbb37d4 7f91bba1
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -1793,6 +1793,17 @@ package android.app.appfunctions {
    field @FlaggedApi("android.permission.flags.app_function_access_ui_enabled") public static final String ACTION_REQUEST_APP_FUNCTION_ACCESS = "android.app.appfunctions.action.REQUEST_APP_FUNCTION_ACCESS";
  }
  @FlaggedApi("android.permission.flags.app_function_access_api_enabled") public static final class AppFunctionManager.AccessHistory implements android.provider.BaseColumns {
    field public static final String COLUMN_ACCESS_TIME = "access_time";
    field public static final String COLUMN_AGENT_PACKAGE_NAME = "agent_package_name";
    field public static final String COLUMN_CUSTOM_INTERACTION_TYPE = "custom_interaction_type";
    field public static final String COLUMN_DURATION = "access_duration";
    field public static final String COLUMN_INTERACTION_TYPE = "interaction_type";
    field public static final String COLUMN_INTERACTION_URI = "interaction_uri";
    field public static final String COLUMN_TARGET_PACKAGE_NAME = "target_package_name";
    field public static final String COLUMN_THREAD_ID = "thread_id";
  }
}
package android.app.assist {
+10 −3
Original line number Diff line number Diff line
@@ -226,8 +226,8 @@ public final class AppFunctionAttribution implements Parcelable {
         * define a set of string constants for these custom interaction types and set them here
         * accordingly.
         *
         * @throws IllegalArgumentException If the interaction type is not
         *   {@link AppFunctionAttribution#INTERACTION_TYPE_OTHER}.
         * @throws IllegalArgumentException If the interaction type is not {@link
         *     AppFunctionAttribution#INTERACTION_TYPE_OTHER}.
         */
        @NonNull
        public Builder setCustomInteractionType(@NonNull String customInteractionType) {
@@ -257,12 +257,19 @@ public final class AppFunctionAttribution implements Parcelable {
            return this;
        }

        // TODO(b/427996654): Update the document to use new Intent action.
        /**
         * Sets a deeplink {@link Uri} to the user request that initiated the app function
         * execution.
         *
         * <p>When set, this URI can be used by privacy settings to display a link in the audit
         * history, allowing users to navigate to the context of the original interaction.
         *
         * <p>For the link to be functional, the provided {@link Uri} <strong>must</strong> be
         * resolvable by an {@link android.content.Intent} with the action {@link
         * android.content.Intent#ACTION_VIEW}. To allow privacy settings to launch your activity,
         * the target {@link android.app.Activity} <strong>must</strong> declare a corresponding
         * {@code <intent-filter>} in your manifest.
         */
        @NonNull
        public Builder setInteractionUri(@Nullable Uri interactionUri) {
+116 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
@@ -35,6 +36,7 @@ import android.annotation.UserHandleAware;
import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException;
import android.app.appsearch.AppSearchManager;
import android.content.Context;
import android.content.Intent;
import android.os.CancellationSignal;
import android.os.ICancellationSignal;
import android.os.OutcomeReceiver;
@@ -42,6 +44,7 @@ import android.os.ParcelableException;
import android.os.RemoteException;
import android.os.SystemClock;
import android.permission.flags.Flags;
import android.provider.BaseColumns;
import android.util.ArraySet;

import com.android.internal.R;
@@ -100,6 +103,119 @@ import java.util.concurrent.Executor;
@SystemService(Context.APP_FUNCTION_SERVICE)
public final class AppFunctionManager {

    // TODO(b/427993624): Expose Uri once ContentProvider is added
    /**
     * The contract between the AppFunction access history provider and applications with read
     * permission. Contains definitions for the supported URIs and columns.
     *
     * <p>This class provides access to the history of AppFunction calls. The access history is
     * stored on a per-user basis. An application querying the access history provider will only see
     * the records for the user it is currently running as.
     *
     * @see AppFunctionAttribution
     * @hide
     */
    @FlaggedApi(Flags.FLAG_APP_FUNCTION_ACCESS_API_ENABLED)
    @SystemApi
    public static final class AccessHistory implements BaseColumns {
        private AccessHistory() {}

        /**
         * The package name of the agent app.
         *
         * <p>Type: TEXT
         */
        public static final String COLUMN_AGENT_PACKAGE_NAME = "agent_package_name";

        /**
         * The package name of the target app.
         *
         * <p>Type: TEXT
         */
        public static final String COLUMN_TARGET_PACKAGE_NAME = "target_package_name";

        /**
         * The type of interaction that triggered the function call. See {@link
         * AppFunctionAttribution.InteractionType} for a list of possible values.
         *
         * <p>The column is nullable. The caller should call {@link android.database.Cursor#isNull}
         * to check if the column value is null for that row.
         *
         * <p>Type: INTEGER (int)
         */
        @SuppressLint("IntentName")
        public static final String COLUMN_INTERACTION_TYPE = "interaction_type";

        /**
         * The custom interaction type, used when {@link
         * AppFunctionAttribution#getInteractionType()} is {@link
         * AppFunctionAttribution#INTERACTION_TYPE_OTHER}.
         *
         * <p>The column is nullable. The caller should call {@link android.database.Cursor#isNull}
         * to check if the column value is null for that row.
         *
         * <p>Type: TEXT
         */
        @SuppressLint("IntentName")
        public static final String COLUMN_CUSTOM_INTERACTION_TYPE = "custom_interaction_type";

        /**
         * A URI linking to the original interaction context.
         *
         * <p>The column is nullable. The caller should call {@link android.database.Cursor#isNull}
         * to check if the column value is null for that row.
         *
         * <p>To launch this URI, the caller must construct an explicit {@link
         * android.content.Intent}. An implicit Intent is not sufficient and may not resolve to the
         * correct component. The required procedure is as follows:
         *
         * <ol>
         *   <li>Create an {@link android.content.Intent} with this URI as its data.
         *   <li>Call {@link android.content.Intent#setPackage(String)} on the Intent, providing the
         *       package name from {@link AccessHistory#COLUMN_AGENT_PACKAGE_NAME}.
         *   <li>Resolve the target activity by calling {@link
         *       android.content.pm.PackageManager#resolveActivity(Intent, int)}.
         *   <li>If the returned {@link android.content.pm.ResolveInfo} and its nested {@code
         *       activityInfo} are not null, create an explicit Intent.
         *   <li>Make the Intent explicit by calling {@link
         *       android.content.Intent#setComponent(android.content.ComponentName)}, creating the
         *       {@code ComponentName} from the {@code packageName} and {@code name} fields within
         *       the {@link android.content.pm.ResolveInfo#activityInfo}.
         *   <li>The resulting explicit Intent can now be used to start the activity.
         * </ol>
         *
         * <p>Type: TEXT
         *
         * @see AppFunctionAttribution.Builder#setInteractionUri
         */
        @SuppressLint("IntentName")
        public static final String COLUMN_INTERACTION_URI = "interaction_uri";

        /**
         * An identifier to group related function calls.
         *
         * <p>The column is nullable. The caller should call {@link android.database.Cursor#isNull}
         * to check if the column value is null for that row.
         *
         * <p>Type: TEXT
         */
        public static final String COLUMN_THREAD_ID = "thread_id";

        /**
         * The timestamp (in milliseconds) when the app function was accessed.
         *
         * <p>Type: INTEGER (long)
         */
        public static final String COLUMN_ACCESS_TIME = "access_time";

        /**
         * The duration (in milliseconds) of the app function execution.
         *
         * <p>Type: INTEGER (long)
         */
        public static final String COLUMN_DURATION = "access_duration";
    }

    /**
     * Activity action: Launch UI that shows list of all agents and provides management of App
     * Function access of those agents.
+246 −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.appfunctions;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.app.appfunctions.AppFunctionAttribution;
import android.app.appfunctions.AppFunctionManager.AccessHistory;
import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.util.Log;
import android.util.Slog;

import java.util.Objects;

/** The database helper for managing AppFunction access histories. */
public final class AppFunctionAccessDatabaseHelper extends SQLiteOpenHelper {

    private static final class AccessHistoryTable {
        static final String DB_TABLE = "appfunction_access";

        static final String CREATE_TABLE_SQL =
                "CREATE TABLE IF NOT EXISTS "
                        + DB_TABLE
                        + " ("
                        + AccessHistory._ID
                        + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                        + AccessHistory.COLUMN_AGENT_PACKAGE_NAME
                        + " TEXT, "
                        + AccessHistory.COLUMN_TARGET_PACKAGE_NAME
                        + " TEXT, "
                        + AccessHistory.COLUMN_INTERACTION_TYPE
                        + " INTEGER, "
                        + AccessHistory.COLUMN_CUSTOM_INTERACTION_TYPE
                        + " TEXT, "
                        + AccessHistory.COLUMN_INTERACTION_URI
                        + " TEXT, "
                        + AccessHistory.COLUMN_THREAD_ID
                        + " TEXT, "
                        + AccessHistory.COLUMN_ACCESS_TIME
                        + " INTEGER, "
                        + AccessHistory.COLUMN_DURATION
                        + " INTEGER);";

        static final String DELETE_TABLE_DATA_BEFORE_ACCESS_TIME =
                "DELETE FROM " + DB_TABLE + " WHERE " + AccessHistory.COLUMN_ACCESS_TIME + " < ?";

        static final String DELETE_TABLE_DATA_FOR_PACKAGE =
                "DELETE FROM "
                        + DB_TABLE
                        + " WHERE "
                        + AccessHistory.COLUMN_AGENT_PACKAGE_NAME
                        + " = ?"
                        + " OR "
                        + AccessHistory.COLUMN_TARGET_PACKAGE_NAME
                        + " = ?";

        static final String DELETE_TABLE_DATA = "DELETE FROM " + DB_TABLE;
    }

    private static final String DB_NAME = "appfunction_access.db";

    private static final int DB_VERSION = 1;

    private static final String TAG = "AppFunctionDatabase";

    AppFunctionAccessDatabaseHelper(@NonNull Context context) {
        super(context, DB_NAME, /* factory= */ null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        try {
            db.execSQL(AccessHistoryTable.CREATE_TABLE_SQL);
        } catch (Exception e) {
            Log.e(TAG, "CreateTable: Failed.");
            throw e;
        }
    }

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

    /** Queries the AppFunction access histories. */
    @Nullable
    public Cursor queryAppFunctionAccessHistory(
            @Nullable String[] projection,
            @Nullable String selection,
            @Nullable String[] selectionArgs,
            @Nullable String sortOrder) {
        try {
            final SQLiteDatabase db = getReadableDatabase();
            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
            qb.setStrict(/* strict= */ true);
            qb.setTables(AccessHistoryTable.DB_TABLE);
            Cursor cursor =
                    qb.query(
                            db,
                            projection,
                            selection,
                            selectionArgs,
                            /* groupBy= */ null,
                            /* having= */ null,
                            sortOrder);
            if (cursor == null) {
                Log.e(TAG, "Query AppFunction access histories: Failed.");
                return null;
            }
            return cursor;
        } catch (Exception e) {
            Log.e(TAG, "Query AppFunction access histories: Failed.", e);
            return null;
        }
    }

    /**
     * Inserts an AppFunction access history to the given database.
     *
     * @return The row id or -1 if fail to insert.
     */
    @WorkerThread
    public long insertAppFunctionAccessHistory(
            @NonNull ExecuteAppFunctionAidlRequest aidlRequest, long duration) {
        Objects.requireNonNull(aidlRequest);

        try {
            final SQLiteDatabase db = getWritableDatabase();
            final ContentValues values =
                    prepareAppFunctionAccessContentValue(aidlRequest, duration);

            return db.insert(AccessHistoryTable.DB_TABLE, /* nullColumnHack= */ null, values);
        } catch (Exception e) {
            Slog.e(TAG, "Insert AppFunction access histories: Failed.", e);
            return -1;
        }
    }

    @NonNull
    private ContentValues prepareAppFunctionAccessContentValue(
            @NonNull ExecuteAppFunctionAidlRequest aidlRequest, long duration) {
        Objects.requireNonNull(aidlRequest);

        final ContentValues values = new ContentValues();

        values.put(
                AccessHistory.COLUMN_AGENT_PACKAGE_NAME,
                Objects.requireNonNull(aidlRequest.getCallingPackage()));
        final String targetPackage =
                Objects.requireNonNull(aidlRequest.getClientRequest().getTargetPackageName());
        values.put(AccessHistory.COLUMN_TARGET_PACKAGE_NAME, targetPackage);
        values.put(AccessHistory.COLUMN_ACCESS_TIME, aidlRequest.getRequestTime());
        values.put(AccessHistory.COLUMN_DURATION, duration);

        final AppFunctionAttribution attribution = aidlRequest.getClientRequest().getAttribution();

        if (attribution != null) {
            values.put(AccessHistory.COLUMN_INTERACTION_TYPE, attribution.getInteractionType());
            final String customInteractionType = attribution.getCustomInteractionType();
            if (customInteractionType != null) {
                values.put(AccessHistory.COLUMN_CUSTOM_INTERACTION_TYPE, customInteractionType);
            }
            final Uri interactionUri = attribution.getInteractionUri();
            if (interactionUri != null) {
                values.put(AccessHistory.COLUMN_INTERACTION_URI, interactionUri.toString());
            }
            final String threadId = attribution.getThreadId();
            if (threadId != null) {
                values.put(AccessHistory.COLUMN_THREAD_ID, threadId);
            }
        }

        return values;
    }

    /**
     * Deletes expired AppFunction access histories from the database.
     *
     * @param retentionMillis The maximum age of records to keep, in milliseconds. Records older
     *     than this will be deleted.
     */
    @WorkerThread
    public void deleteExpiredAppFunctionAccessHistories(long retentionMillis) {
        try {
            final SQLiteDatabase db = getWritableDatabase();
            final long cutOffTimestamp = System.currentTimeMillis() - retentionMillis;

            final String[] whereArgs = {String.valueOf(cutOffTimestamp)};
            db.execSQL(AccessHistoryTable.DELETE_TABLE_DATA_BEFORE_ACCESS_TIME, whereArgs);
        } catch (Exception e) {
            Slog.e(TAG, "Delete expired AppFunction access histories: Failed", e);
        }
    }

    /** Deletes AppFunction access histories that are associated with the given packageName. */
    @WorkerThread
    public void deleteAppFunctionAccessHistories(@NonNull String packageName) {
        Objects.requireNonNull(packageName);

        try {
            final SQLiteDatabase db = getWritableDatabase();

            final String[] whereArgs = {packageName, packageName};
            db.execSQL(AccessHistoryTable.DELETE_TABLE_DATA_FOR_PACKAGE, whereArgs);
        } catch (Exception e) {
            Slog.e(
                    TAG,
                    "Delete AppFunction access histories for "
                            + "{packageName="
                            + packageName
                            + "} : Failed",
                    e);
        }
    }

    /** Deletes all AppFunction access histories. */
    @WorkerThread
    public void deleteAll() {
        try {
            final SQLiteDatabase db = getWritableDatabase();
            db.execSQL(AccessHistoryTable.DELETE_TABLE_DATA);
        } catch (Exception e) {
            Slog.e(TAG, "Delete all AppFunction access histories: Failed.", e);
        }
    }
}
+407 −0

File added.

Preview size limit exceeded, changes collapsed.