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

Commit f2a66100 authored by MingWei's avatar MingWei
Browse files

Add AppFunctionAttribution APIs

Flag: android.permission.flags.app_function_access_api_enabled
Bug: 421120645
Test: atest AppFunctionAttributionTest
Change-Id: Icc421654a8d506114de9c9524bf9435d5d394ff6
parent c92c6491
Loading
Loading
Loading
Loading
+23 −0
Original line number Diff line number Diff line
@@ -8926,6 +8926,27 @@ package android.app.admin {
package android.app.appfunctions {
  @FlaggedApi("android.permission.flags.app_function_access_api_enabled") public final class AppFunctionAttribution implements android.os.Parcelable {
    method public int describeContents();
    method @Nullable public String getCustomInteractionType();
    method public int getInteractionType();
    method @Nullable public android.net.Uri getInteractionUri();
    method @Nullable public String getThreadId();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.app.appfunctions.AppFunctionAttribution> CREATOR;
    field public static final int INTERACTION_TYPE_OTHER = 0; // 0x0
    field public static final int INTERACTION_TYPE_USER_QUERY = 1; // 0x1
    field public static final int INTERACTION_TYPE_USER_SCHEDULED = 2; // 0x2
  }
  public static final class AppFunctionAttribution.Builder {
    ctor public AppFunctionAttribution.Builder(int);
    method @NonNull public android.app.appfunctions.AppFunctionAttribution build();
    method @NonNull public android.app.appfunctions.AppFunctionAttribution.Builder setCustomInteractionType(@NonNull String);
    method @NonNull public android.app.appfunctions.AppFunctionAttribution.Builder setInteractionUri(@Nullable android.net.Uri);
    method @NonNull public android.app.appfunctions.AppFunctionAttribution.Builder setThreadId(@Nullable String);
  }
  @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionException extends java.lang.Exception implements android.os.Parcelable {
    ctor public AppFunctionException(int, @Nullable String);
    ctor public AppFunctionException(int, @Nullable String, @NonNull android.os.Bundle);
@@ -8975,6 +8996,7 @@ package android.app.appfunctions {
  @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class ExecuteAppFunctionRequest implements android.os.Parcelable {
    method public int describeContents();
    method @FlaggedApi("android.permission.flags.app_function_access_api_enabled") @Nullable public android.app.appfunctions.AppFunctionAttribution getAttribution();
    method @NonNull public android.os.Bundle getExtras();
    method @NonNull public String getFunctionIdentifier();
    method @NonNull public android.app.appsearch.GenericDocument getParameters();
@@ -8986,6 +9008,7 @@ package android.app.appfunctions {
  public static final class ExecuteAppFunctionRequest.Builder {
    ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String);
    method @NonNull public android.app.appfunctions.ExecuteAppFunctionRequest build();
    method @FlaggedApi("android.permission.flags.app_function_access_api_enabled") @NonNull public android.app.appfunctions.ExecuteAppFunctionRequest.Builder setAttribution(@NonNull android.app.appfunctions.AppFunctionAttribution);
    method @NonNull public android.app.appfunctions.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle);
    method @NonNull public android.app.appfunctions.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument);
  }
+285 −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 android.app.appfunctions;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.permission.flags.Flags;

import androidx.annotation.NonNull;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;

/**
 * Represents attribution information for an app function execution, detailing the context and
 * nature of the interaction that triggered it. This information can be used by the privacy setting
 * to provide transparency to the user about why an app function was invoked.
 *
 * @see ExecuteAppFunctionRequest#getAttribution
 * @see ExecuteAppFunctionRequest.Builder#setAttribution
 */
@FlaggedApi(Flags.FLAG_APP_FUNCTION_ACCESS_API_ENABLED)
public final class AppFunctionAttribution implements Parcelable {
    @NonNull private static final String TAG = "AppFunctionAttribution";

    @NonNull
    public static final Creator<AppFunctionAttribution> CREATOR =
            new Creator<AppFunctionAttribution>() {
                @Override
                public AppFunctionAttribution createFromParcel(Parcel parcel) {
                    return new AppFunctionAttribution(parcel);
                }

                @Override
                public AppFunctionAttribution[] newArray(int size) {
                    return new AppFunctionAttribution[0];
                }
            };

    /**
     * Indicates an interaction type not covered by other predefined constants.
     *
     * <p>When this type is used, a custom interaction type string must be provided via {@link
     * Builder#setCustomInteractionType}.
     *
     * @see AppFunctionAttribution#getCustomInteractionType
     * @see AppFunctionAttribution.Builder#setCustomInteractionType
     */
    public static final int INTERACTION_TYPE_OTHER = 0;

    /**
     * Indicates that the app function execution was triggered as a direct result of a user query.
     */
    public static final int INTERACTION_TYPE_USER_QUERY = 1;

    /** Indicates that the app function execution was triggered by a user-scheduled task. */
    public static final int INTERACTION_TYPE_USER_SCHEDULED = 2;

    /** @hide */
    @IntDef(
            prefix = "INTERACTION_TYPE_",
            value = {
                INTERACTION_TYPE_OTHER,
                INTERACTION_TYPE_USER_QUERY,
                INTERACTION_TYPE_USER_SCHEDULED,
            })
    @Retention(RetentionPolicy.SOURCE)
    public @interface InteractionType {}

    private final int mInteractionType;

    @Nullable private final String mCustomInteractionType;

    @Nullable private final String mThreadId;

    @Nullable private final Uri mInteractionUri;

    private AppFunctionAttribution(
            @InteractionType int interactionType,
            @Nullable String customInteractionType,
            @Nullable String threadId,
            @Nullable Uri interactionUri) {
        mInteractionType = interactionType;
        if (interactionType == INTERACTION_TYPE_OTHER && customInteractionType == null) {
            throw new IllegalArgumentException(
                    "Must set customInteractionType when interactionType=INTERACTION_TYPE_OTHER");
        }
        if (customInteractionType != null && interactionType != INTERACTION_TYPE_OTHER) {
            throw new IllegalArgumentException(
                    "customInteractionType is only allowed when "
                            + "interactionType=INTERACTION_TYPE_OTHER");
        }
        mCustomInteractionType = customInteractionType;
        mThreadId = threadId;
        mInteractionUri = interactionUri;
    }

    private AppFunctionAttribution(@NonNull Parcel in) {
        mInteractionType = in.readInt();
        mCustomInteractionType = in.readString8();
        mThreadId = in.readString8();
        mInteractionUri = in.readTypedObject(Uri.CREATOR);
    }

    /** Returns the type of interaction that triggered the app function execution. */
    @InteractionType
    public int getInteractionType() {
        return mInteractionType;
    }

    /**
     * Returns the custom string describing the interaction, if {@link #getInteractionType()} is
     * {@link AppFunctionAttribution#INTERACTION_TYPE_OTHER}.
     */
    @Nullable
    public String getCustomInteractionType() {
        return mCustomInteractionType;
    }

    /** Returns the unique thread ID associated with this app function execution. */
    @Nullable
    public String getThreadId() {
        return mThreadId;
    }

    /** Returns the {@link Uri} linking to the original interaction. */
    @Nullable
    public Uri getInteractionUri() {
        return mInteractionUri;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(mInteractionType);
        dest.writeString8(mCustomInteractionType);
        dest.writeString8(mThreadId);
        dest.writeTypedObject(mInteractionUri, flags);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        AppFunctionAttribution that = (AppFunctionAttribution) o;
        return mInteractionType == that.mInteractionType
                && Objects.equals(mCustomInteractionType, that.mCustomInteractionType)
                && Objects.equals(mThreadId, that.mThreadId)
                && Objects.equals(mInteractionUri, that.mInteractionUri);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mInteractionType, mCustomInteractionType, mThreadId, mInteractionUri);
    }

    @Override
    public String toString() {
        return "AppFunctionAttribution("
                + "interactionType="
                + mInteractionType
                + ","
                + "customInteractionType="
                + mCustomInteractionType
                + ","
                + "threadId="
                + mThreadId
                + ","
                + "interactionUri="
                + mInteractionUri
                + ")";
    }

    /** Builder for {@link AppFunctionAttribution}. */
    public static final class Builder {
        private final int mInteractionType;

        @Nullable private String mCustomInteractionType;

        @Nullable private String mThreadId;

        @Nullable private Uri mInteractionUri;

        /**
         * Creates a new instance of this builder class.
         *
         * @param interactionType The interaction type. Must be one of {@link
         *     AppFunctionAttribution#INTERACTION_TYPE_OTHER}, {@link
         *     AppFunctionAttribution#INTERACTION_TYPE_USER_QUERY}, or {@link
         *     AppFunctionAttribution#INTERACTION_TYPE_USER_SCHEDULED}. If {@link
         *     AppFunctionAttribution#INTERACTION_TYPE_OTHER} is used, {@link
         *     #setCustomInteractionType(String)} must also be called.
         */
        public Builder(@InteractionType int interactionType) {
            mInteractionType = interactionType;
        }

        /**
         * Sets the custom interaction type to describe the interaction.
         *
         * <p>This method must be called if and only if the {@code interactionType} provided to the
         * constructor was {@link AppFunctionAttribution#INTERACTION_TYPE_OTHER}. The caller should
         * 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}.
         */
        @NonNull
        public Builder setCustomInteractionType(@NonNull String customInteractionType) {
            if (mInteractionType != AppFunctionAttribution.INTERACTION_TYPE_OTHER) {
                throw new IllegalArgumentException(
                        "Cannot set customInteractionType because the interaction type is not "
                                + "INTERACTION_TYPE_OTHER");
            }
            mCustomInteractionType = Objects.requireNonNull(customInteractionType);
            return this;
        }

        /**
         * Sets a unique thread ID.
         *
         * <p>If a set of consequent AppFunctions are part of the same user interaction, an agent
         * should indicate this by specifying the same identifier across different AppFunction
         * calls. This information, if present, can be used in privacy settings to visually group
         * multiple AppFunctions together, aiding users in auditing related operations.
         *
         * <p>For example, this id can be the chat thread where the app function execution request
         * was initiated from.
         */
        @NonNull
        public Builder setThreadId(@Nullable String threadId) {
            mThreadId = threadId;
            return this;
        }

        /**
         * 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.
         */
        @NonNull
        public Builder setInteractionUri(@Nullable Uri interactionUri) {
            mInteractionUri = interactionUri;
            return this;
        }

        /** Builds the {@link AppFunctionAttribution}. */
        @NonNull
        public AppFunctionAttribution build() {
            if (mInteractionType == INTERACTION_TYPE_OTHER && mCustomInteractionType == null) {
                throw new IllegalArgumentException(
                        "Must set customInteractionType since"
                            + " interactionType=INTERACTION_TYPE_OTHER");
            }
            return new AppFunctionAttribution(
                    mInteractionType, mCustomInteractionType, mThreadId, mInteractionUri);
        }
    }
}
+67 −10
Original line number Diff line number Diff line
@@ -20,10 +20,12 @@ import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANA

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.appsearch.GenericDocument;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.permission.flags.Flags;

import java.util.Objects;

@@ -44,13 +46,27 @@ public final class ExecuteAppFunctionRequest implements Parcelable {
            new Creator<>() {
                @Override
                public ExecuteAppFunctionRequest createFromParcel(Parcel parcel) {
                    String targetPackageName = parcel.readString8();
                    String functionIdentifier = parcel.readString8();
                    String targetPackageName = Objects.requireNonNull(parcel.readString8());
                    String functionIdentifier = Objects.requireNonNull(parcel.readString8());
                    GenericDocumentWrapper parameters =
                            GenericDocumentWrapper.CREATOR.createFromParcel(parcel);
                    Bundle extras = parcel.readBundle(Bundle.class.getClassLoader());
                            Objects.requireNonNull(
                                    GenericDocumentWrapper.CREATOR.createFromParcel(parcel));
                    Bundle extras =
                            Objects.requireNonNull(
                                    parcel.readBundle(Bundle.class.getClassLoader()));
                    if (Flags.appFunctionAccessApiEnabled()) {
                        final AppFunctionAttribution attribution = parcel.readTypedObject(
                                AppFunctionAttribution.CREATOR);
                        return new ExecuteAppFunctionRequest(
                            targetPackageName, functionIdentifier, extras, parameters);
                                targetPackageName,
                                functionIdentifier,
                                extras,
                                parameters,
                                attribution);
                    } else {
                        return new ExecuteAppFunctionRequest(
                                targetPackageName, functionIdentifier, extras, parameters, null);
                    }
                }

                @Override
@@ -81,15 +97,19 @@ public final class ExecuteAppFunctionRequest implements Parcelable {
     */
    @NonNull private final GenericDocumentWrapper mParameters;

    @Nullable private final AppFunctionAttribution mAttribution;

    private ExecuteAppFunctionRequest(
            @NonNull String targetPackageName,
            @NonNull String functionIdentifier,
            @NonNull Bundle extras,
            @NonNull GenericDocumentWrapper parameters) {
            @NonNull GenericDocumentWrapper parameters,
            @Nullable AppFunctionAttribution attribution) {
        mTargetPackageName = Objects.requireNonNull(targetPackageName);
        mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
        mExtras = Objects.requireNonNull(extras);
        mParameters = Objects.requireNonNull(parameters);
        mAttribution = attribution;
    }

    /** Returns the package name of the app that hosts the function. */
@@ -136,14 +156,26 @@ public final class ExecuteAppFunctionRequest implements Parcelable {
        return mExtras;
    }

    /**
     * Returns the {@link AppFunctionAttribution} represents attribution information for the {@link
     * ExecuteAppFunctionRequest}.
     */
    @FlaggedApi(Flags.FLAG_APP_FUNCTION_ACCESS_API_ENABLED)
    @Nullable
    public AppFunctionAttribution getAttribution() {
        return mAttribution;
    }

    /**
     * Returns the size of the request in bytes.
     *
     * @hide
     */
    public int getRequestDataSize() {
        return mTargetPackageName.getBytes().length + mFunctionIdentifier.getBytes().length
                + mParameters.getDataSize() + mExtras.getSize();
        return mTargetPackageName.getBytes().length
                + mFunctionIdentifier.getBytes().length
                + mParameters.getDataSize()
                + mExtras.getSize();
    }

    @Override
@@ -152,6 +184,9 @@ public final class ExecuteAppFunctionRequest implements Parcelable {
        dest.writeString8(mFunctionIdentifier);
        mParameters.writeToParcel(dest, flags);
        dest.writeBundle(mExtras);
        if (Flags.appFunctionAccessApiEnabled()) {
            dest.writeTypedObject(mAttribution, flags);
        }
    }

    @Override
@@ -168,6 +203,8 @@ public final class ExecuteAppFunctionRequest implements Parcelable {
        @NonNull
        private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build();

        @Nullable private AppFunctionAttribution mAttribution = null;

        /**
         * Creates a new instance of this builder class.
         *
@@ -200,6 +237,25 @@ public final class ExecuteAppFunctionRequest implements Parcelable {
            return this;
        }

        /**
         * Sets the {@link AppFunctionAttribution}.
         *
         * <p>Provides the attribution information for an {@link ExecuteAppFunctionRequest}. This
         * information can be used by the privacy setting to provide transparency to the user about
         * why an app function was invoked.
         *
         * <p>This is currently optional,  but may become required for apps targeting a future
         * release.
         *
         * @see AppFunctionAttribution
         */
        @FlaggedApi(Flags.FLAG_APP_FUNCTION_ACCESS_API_ENABLED)
        @NonNull
        public Builder setAttribution(@NonNull AppFunctionAttribution attribution) {
            mAttribution = Objects.requireNonNull(attribution);
            return this;
        }

        /** Builds the {@link ExecuteAppFunctionRequest}. */
        @NonNull
        public ExecuteAppFunctionRequest build() {
@@ -207,7 +263,8 @@ public final class ExecuteAppFunctionRequest implements Parcelable {
                    mTargetPackageName,
                    mFunctionIdentifier,
                    mExtras,
                    new GenericDocumentWrapper(mParameters));
                    new GenericDocumentWrapper(mParameters),
                    mAttribution);
        }
    }
}