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

Commit 9a55bf35 authored by William Loh's avatar William Loh
Browse files

Update statementservice assetlink.json parsing

This updates the statement service json parser to parse the new
relation_extensions introduced to the digital assetlinks protocol for
dynamic app links.

The parser is also changed to take the last value in a json object if
multiple values for the same key is found. This is done to make it
consistent with the json parser used in the DAL service used by GMS Core
devices.

Bug: 307557449
Test: manual
Flag: EXEMPT external library
Change-Id: Id550a53f2aa932959f27ecdb9556b6dde0c5fb52
parent 88f97ae5
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ android_app {
    privileged: true,
    certificate: "platform",
    static_libs: [
        "StatementServiceParser",
        "androidx.appcompat_appcompat",
        "androidx.collection_collection-ktx",
        "androidx.work_work-runtime",
+35 −1
Original line number Diff line number Diff line
@@ -28,6 +28,8 @@ import java.io.StringReader
import java.util.ArrayList
import com.android.statementservice.retriever.WebAsset
import com.android.statementservice.retriever.AndroidAppAsset
import com.android.statementservice.retriever.DynamicAppLinkComponent
import org.json.JSONObject

/**
 * Parses JSON from the Digital Asset Links specification. For examples, see [WebAsset],
@@ -97,13 +99,45 @@ object StatementParser {
                FIELD_NOT_ARRAY_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION)
            )
        val target = AssetFactory.create(targetObject)
        val dynamicAppLinkComponents = parseDynamicAppLinkComponents(
            statement.optJSONObject(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS)
        )

        val statements = (0 until relations.length())
            .map { relations.getString(it) }
            .map(Relation::create)
            .map { Statement.create(source, target, it) }
            .map { Statement.create(source, target, it, dynamicAppLinkComponents) }
        return Result.Success(ParsedStatement(statements, listOfNotNull(delegate)))
    }

    private fun parseDynamicAppLinkComponents(
        statement: JSONObject?
    ): List<DynamicAppLinkComponent> {
        val relationExtensions = statement?.optJSONObject(
            StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS
        ) ?: return emptyList()
        val handleAllUrlsRelationExtension = relationExtensions.optJSONObject(
            StatementUtils.RELATION.toString()
        ) ?: return emptyList()
        val components = handleAllUrlsRelationExtension.optJSONArray(
            StatementUtils.RELATION_EXTENSION_FIELD_DAL_COMPONENTS
        ) ?: return emptyList()

        return (0 until components.length())
            .map { components.getJSONObject(it) }
            .map { parseComponent(it) }
    }

    private fun parseComponent(component: JSONObject): DynamicAppLinkComponent {
        val query = component.optJSONObject("?")
        return DynamicAppLinkComponent.create(
            component.optBoolean("exclude", false),
            component.optString("#"),
            component.optString("/"),
            query?.keys()?.asSequence()?.associateWith { query.getString(it) },
            component.optString("comments")
        )
    }

    data class ParsedStatement(val statements: List<Statement>, val delegates: List<String>)
}
+145 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.statementservice.retriever;

import android.annotation.Nullable;

import java.util.Map;

/**
 * A immutable value type representing a dynamic app link component
 */
public final class DynamicAppLinkComponent {
    private final boolean mExclude;
    private final String mFragment;
    private final String mPath;
    private final Map<String, String> mQuery;
    private final String mComments;

    private DynamicAppLinkComponent(boolean exclude, String fragment, String path,
                                    Map<String, String> query, String comments) {
        mExclude = exclude;
        mFragment = fragment;
        mPath = path;
        mQuery = query;
        mComments = comments;
    }

    /**
     * Returns true or false indicating whether this rule should be a exclusion rule.
     */
    public boolean getExclude() {
        return mExclude;
    }

    /**
     * Returns a optional pattern string for matching URL fragments.
     */
    @Nullable
    public String getFragment() {
        return mFragment;
    }

    /**
     * Returns a optional pattern string for matching URL paths.
     */
    @Nullable
    public String getPath() {
        return mPath;
    }

    /**
     * Returns a optional pattern string for matching a single key-value pair in the URL query
     * params.
     */
    @Nullable
    public Map<String, String> getQuery() {
        return mQuery;
    }

    /**
     * Returns a optional comment string for this component.
     */
    @Nullable
    public String getComments() {
        return mComments;
    }

    /**
     * Creates a new DynamicAppLinkComponent object.
     */
    public static DynamicAppLinkComponent create(boolean exclude, String fragment, String path,
                                                 Map<String, String> query, String comments) {
        return new DynamicAppLinkComponent(exclude, fragment, path, query, comments);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        DynamicAppLinkComponent rule = (DynamicAppLinkComponent) o;

        if (mExclude != rule.mExclude) {
            return false;
        }
        if (!mFragment.equals(rule.mFragment)) {
            return false;
        }
        if (!mPath.equals(rule.mPath)) {
            return false;
        }
        if (!mQuery.equals(rule.mQuery)) {
            return false;
        }
        if (!mComments.equals(rule.mComments)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = Boolean.hashCode(mExclude);
        result = 31 * result + mFragment.hashCode();
        result = 31 * result + mPath.hashCode();
        result = 31 * result + mQuery.hashCode();
        result = 31 * result + mComments.hashCode();
        return result;
    }

    @Override
    public String toString() {
        StringBuilder statement = new StringBuilder();
        statement.append("HandleAllUriRule: ");
        statement.append(mExclude);
        statement.append(", ");
        statement.append(mFragment);
        statement.append(", ");
        statement.append(mPath);
        statement.append(", ");
        statement.append(mQuery);
        statement.append(", ");
        statement.append(mComments);
        return statement.toString();
    }
}
+0 −6
Original line number Diff line number Diff line
@@ -46,12 +46,6 @@ public final class JsonParser {
        while (reader.hasNext()) {
            String fieldName = reader.nextName();

            if (output.has(fieldName)) {
                errorMsg = "Duplicate field name.";
                reader.skipValue();
                continue;
            }

            JsonToken token = reader.peek();
            if (token.equals(JsonToken.BEGIN_ARRAY)) {
                output.put(fieldName, new JSONArray(parseArray(reader)));
+46 −5
Original line number Diff line number Diff line
@@ -23,6 +23,10 @@ import com.android.statementservice.network.retriever.StatementRetriever;

import kotlin.coroutines.Continuation;

import java.util.Collections;
import java.util.List;


/**
 * An immutable value type representing a statement, consisting of a source, target, and relation.
 * This reflects an assertion that the relation holds for the source, target pair. For example, if a
@@ -32,7 +36,21 @@ import kotlin.coroutines.Continuation;
 * {
 * "relation": ["delegate_permission/common.handle_all_urls"],
 * "target"  : {"namespace": "android_app", "package_name": "com.example.app",
 *              "sha256_cert_fingerprints": ["00:11:22:33"] }
 *              "sha256_cert_fingerprints": ["00:11:22:33"] },
 * "relation_extensions": {
 *     "delegate_permission/common_handle_all_urls": {
 *         "dynamic_app_link_components": [
 *             {
 *                 "/": "/foo*",
 *                 "exclude": true,
 *                 "comments": "App should not handle paths that start with foo"
 *             },
 *             {
 *                 "/": "*",
 *                 "comments": "Catch all other paths"
 *             }
 *         ]
 *     }
 * }
 * </pre>
 *
@@ -40,7 +58,7 @@ import kotlin.coroutines.Continuation;
 * return a {@link Statement} with {@link #getSource} equal to the input parameter,
 * {@link #getRelation} equal to
 *
 * <pre>Relation.create("delegate_permission", "common.get_login_creds");</pre>
 * <pre>Relation.create("delegate_permission", "common.handle_all_urls");</pre>
 *
 * and with {@link #getTarget} equal to
 *
@@ -48,17 +66,23 @@ import kotlin.coroutines.Continuation;
 *                           + "\"package_name\": \"com.example.app\"}"
 *                           + "\"sha256_cert_fingerprints\": \"[\"00:11:22:33\"]\"}");
 * </pre>
 *
 * If extensions exist for the handle_all_urls relation then {@link #getDynamicAppLinkComponents}
 * will return a list of parsed {@link DynamicAppLinkComponent}s.
 */
public final class Statement {

    private final AbstractAsset mTarget;
    private final Relation mRelation;
    private final AbstractAsset mSource;
    private final List<DynamicAppLinkComponent> mDynamicAppLinkComponents;

    private Statement(AbstractAsset source, AbstractAsset target, Relation relation) {
    private Statement(AbstractAsset source, AbstractAsset target, Relation relation,
                      List<DynamicAppLinkComponent> components) {
        mSource = source;
        mTarget = target;
        mRelation = relation;
        mDynamicAppLinkComponents = Collections.unmodifiableList(components);
    }

    /**
@@ -85,6 +109,14 @@ public final class Statement {
        return mRelation;
    }

    /**
     * Returns the relation matching rules of the statement.
     */
    @NonNull
    public List<DynamicAppLinkComponent> getDynamicAppLinkComponents() {
        return mDynamicAppLinkComponents;
    }

    /**
     * Creates a new Statement object for the specified target asset and relation. For example:
     * <pre>
@@ -95,8 +127,9 @@ public final class Statement {
     * </pre>
     */
    public static Statement create(@NonNull AbstractAsset source, @NonNull AbstractAsset target,
                                   @NonNull Relation relation) {
        return new Statement(source, target, relation);
                                   @NonNull Relation relation,
                                   @NonNull List<DynamicAppLinkComponent> components) {
        return new Statement(source, target, relation, components);
    }

    @Override
@@ -119,6 +152,9 @@ public final class Statement {
        if (!mSource.equals(statement.mSource)) {
            return false;
        }
        if (!mDynamicAppLinkComponents.equals(statement.mDynamicAppLinkComponents)) {
            return false;
        }

        return true;
    }
@@ -128,6 +164,7 @@ public final class Statement {
        int result = mTarget.hashCode();
        result = 31 * result + mRelation.hashCode();
        result = 31 * result + mSource.hashCode();
        result = 31 * result + mDynamicAppLinkComponents.hashCode();
        return result;
    }

@@ -140,6 +177,10 @@ public final class Statement {
        statement.append(mTarget);
        statement.append(", ");
        statement.append(mRelation);
        if (!mDynamicAppLinkComponents.isEmpty()) {
            statement.append(", ");
            statement.append(mDynamicAppLinkComponents);
        }
        return statement.toString();
    }
}
Loading