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

Commit a8ea4ef5 authored by Tony Mak's avatar Tony Mak Committed by Android (Google) Code Review
Browse files

Merge "Add shell command to list app functions in JSON" into main

parents b248a66d 60fc65e4
Loading
Loading
Loading
Loading
+17 −8
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;

public final class AppFunctionDumpHelper {
    private static final String TAG = AppFunctionDumpHelper.class.getSimpleName();
@@ -89,20 +90,19 @@ public final class AppFunctionDumpHelper {
        return false;
    }

    private static void dumpAppFunctionsStateForUser(
            @NonNull Context context, @NonNull IndentingPrintWriter pw, boolean isVerbose) {
    /** Returns a map where the key is the package name and the value is its appfunction state. */
    static Map<String, List<SearchResult>> queryAppFunctionsStateForUser(
            @NonNull Context context, boolean isVerbose)
            throws ExecutionException, InterruptedException {
        AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class);
        if (appSearchManager == null) {
            pw.println("Couldn't retrieve AppSearchManager.");
            return;
            throw new IllegalStateException("Couldn't retrieve AppSearchManager.");
        }

        Map<String, List<SearchResult>> packageSearchResults = new ArrayMap<>();

        try (FutureGlobalSearchSession searchSession =
                new FutureGlobalSearchSession(appSearchManager, Runnable::run)) {
            pw.println();

            try (FutureSearchResults futureSearchResults =
                    searchSession
                            .search("", buildAppFunctionMetadataSearchSpec(isVerbose))
@@ -122,10 +122,17 @@ public final class AppFunctionDumpHelper {
                                .add(searchResult);
                    }
                } while (!searchResultsList.isEmpty());

                dumpSearchResults(pw, packageSearchResults);
            }
        }
        return packageSearchResults;
    }

    private static void dumpAppFunctionsStateForUser(
            @NonNull Context context, @NonNull IndentingPrintWriter pw, boolean isVerbose) {
        try {
            Map<String, List<SearchResult>> packageSearchResults =
                    queryAppFunctionsStateForUser(context, isVerbose);
            dumpSearchResults(pw, packageSearchResults);
        } catch (Exception e) {
            pw.println("Failed to dump AppFunction state: " + e);
        }
@@ -134,10 +141,12 @@ public final class AppFunctionDumpHelper {
    private static void dumpSearchResults(
            IndentingPrintWriter pw, Map<String, List<SearchResult>> packageSearchResults) {
        for (Map.Entry<String, List<SearchResult>> entry : packageSearchResults.entrySet()) {
            pw.println();
            pw.println("AppFunctionDocument(s) for package: " + entry.getKey());

            pw.increaseIndent();
            for (SearchResult result : entry.getValue()) {
                pw.println();
                if (result.getGenericDocument()
                        .getSchemaType()
                        .startsWith(AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE)) {
+1 −1
Original line number Diff line number Diff line
@@ -225,7 +225,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
            @NonNull String[] args,
            ShellCallback callback,
            @NonNull ResultReceiver resultReceiver) {
        new AppFunctionManagerServiceShellCommand(this)
        new AppFunctionManagerServiceShellCommand(mContext, this)
                .exec(this, in, out, err, args, callback, resultReceiver);
    }

+91 −96
Original line number Diff line number Diff line
@@ -20,6 +20,10 @@ import static android.app.appfunctions.AppFunctionManager.ACCESS_FLAG_MASK_OTHER
import static android.app.appfunctions.AppFunctionManager.ACCESS_FLAG_OTHER_DENIED;
import static android.app.appfunctions.AppFunctionManager.ACCESS_FLAG_OTHER_GRANTED;

import static com.android.server.appfunctions.AppSearchDataJsonConverter.convertGenericDocumentsToJsonArray;
import static com.android.server.appfunctions.AppSearchDataJsonConverter.convertJsonToGenericDocument;
import static com.android.server.appfunctions.AppSearchDataJsonConverter.searchResultToJsonObject;

import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.appfunctions.AppFunctionException;
@@ -31,6 +35,8 @@ import android.app.appfunctions.IAppFunctionEnabledCallback;
import android.app.appfunctions.IAppFunctionManager;
import android.app.appfunctions.IExecuteAppFunctionCallback;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.SearchResult;
import android.content.Context;
import android.os.Binder;
import android.os.ICancellationSignal;
import android.os.Process;
@@ -44,18 +50,21 @@ import org.json.JSONException;
import org.json.JSONObject;

import java.io.PrintWriter;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/** Shell command implementation for the {@link AppFunctionManagerService}. */
public class AppFunctionManagerServiceShellCommand extends ShellCommand {
    @NonNull private final Context mContext;
    @NonNull private final IAppFunctionManager mService;

    @NonNull
    private final IAppFunctionManager mService;

    AppFunctionManagerServiceShellCommand(@NonNull IAppFunctionManager service) {
    AppFunctionManagerServiceShellCommand(
            @NonNull Context context, @NonNull IAppFunctionManager service) {
        mContext = Objects.requireNonNull(context);
        mService = Objects.requireNonNull(service);
    }

@@ -66,11 +75,18 @@ public class AppFunctionManagerServiceShellCommand extends ShellCommand {
        pw.println("  help");
        pw.println("    Prints this help text.");
        pw.println();
        pw.println("  list-app-functions [--user <USER_ID>]");
        pw.println("    Lists all app functions for a specified user in JSON.");
        pw.println(
                "    --user <USER_ID> (optional): The user ID to list functions for. "
                        + "Defaults to the current user.");
        pw.println();
        pw.println(
                "  execute-app-function --package <PACKAGE_NAME> --function <FUNCTION_ID> "
                        + "--parameters <PARAMETERS_JSON> [--user <USER_ID>]");
        pw.println(
                "    Executes an app function for the given package with the provided parameters.");
                "    Executes an app function for the given package with the provided parameters "
                        + " and returns the result as a JSON string");
        pw.println("    --package <PACKAGE_NAME>: The target package name.");
        pw.println("    --function <FUNCTION_ID>: The ID of the app function to execute.");
        pw.println(
@@ -133,6 +149,8 @@ public class AppFunctionManagerServiceShellCommand extends ShellCommand {

        try {
            switch (cmd) {
                case "list-app-functions":
                    return runListAppFunctions();
                case "execute-app-function":
                    return runExecuteAppFunction();
                case "set-enabled":
@@ -150,6 +168,49 @@ public class AppFunctionManagerServiceShellCommand extends ShellCommand {
        return -1;
    }

    private int runListAppFunctions() throws Exception {
        final PrintWriter pw = getOutPrintWriter();
        int userId = ActivityManager.getCurrentUser();
        String opt;

        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "--user":
                    try {
                        userId = UserHandle.parseUserArg(getNextArgRequired());
                    } catch (NumberFormatException e) {
                        pw.println("Invalid user ID: " + getNextArg() + ". Using current user.");
                    }
                    break;
                default:
                    pw.println("Unknown option: " + opt);
                    return -1;
            }
        }

        Context context = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
        long token = Binder.clearCallingIdentity();
        try {
            Map<String, List<SearchResult>> perPackageSearchResult =
                    AppFunctionDumpHelper.queryAppFunctionsStateForUser(
                            context, /* isVerbose= */ true);
            JSONObject jsonObject = new JSONObject();
            for (Map.Entry<String, List<SearchResult>> entry : perPackageSearchResult.entrySet()) {
                JSONArray searchResults = new JSONArray();
                for (SearchResult result : entry.getValue()) {
                    searchResults.put(searchResultToJsonObject(result));
                }
                jsonObject.put(entry.getKey(), searchResults);
            }
            pw.println(jsonObject.toString(/* indentSpaces =*/ 2));
        } finally {
            Binder.restoreCallingIdentity(token);
        }
        pw.flush();

        return 0;
    }

    private int runSetAppFunctionEnabled() throws Exception {
        final PrintWriter pw = getOutPrintWriter();
        String packageName = null;
@@ -284,7 +345,7 @@ public class AppFunctionManagerServiceShellCommand extends ShellCommand {
            return -1;
        }

        GenericDocument parameters = parseJsonToGenericDocument(parametersJson);
        GenericDocument parameters = convertJsonToGenericDocument(parametersJson);

        ExecuteAppFunctionAidlRequest request =
                new ExecuteAppFunctionAidlRequest(
@@ -296,15 +357,31 @@ public class AppFunctionManagerServiceShellCommand extends ShellCommand {
                        SystemClock.elapsedRealtime());

        CountDownLatch countDownLatch = new CountDownLatch(1);

        final AtomicInteger resultCode = new AtomicInteger(0);
        IExecuteAppFunctionCallback callback =
                new IExecuteAppFunctionCallback.Stub() {

                    @Override
                    public void onSuccess(ExecuteAppFunctionResponse response) {
                        pw.println("App function executed successfully.");
                        pw.println("Function return:");
                        pw.println(response.getResultDocument().toString());
                        try {
                            GenericDocument[] functionReturn =
                                    response.getResultDocument()
                                            .getPropertyDocumentArray(
                                                    ExecuteAppFunctionResponse
                                                            .PROPERTY_RETURN_VALUE);
                            if (functionReturn == null || functionReturn.length == 0) {
                                pw.println(new JSONObject());
                                return;
                            }
                            // HACK: GenericDocument doesn't tell whether a property is singular
                            // or repeated. We always assume the return is an array here.
                            JSONArray functionReturnJson =
                                    convertGenericDocumentsToJsonArray(functionReturn);
                            pw.println(functionReturnJson.toString(/*indentSpace=*/ 2));
                        } catch (JSONException e) {
                            pw.println("Failed to convert the function response to JSON.");
                            resultCode.set(-1);
                        }
                        countDownLatch.countDown();
                    }

@@ -312,6 +389,7 @@ public class AppFunctionManagerServiceShellCommand extends ShellCommand {
                    public void onError(AppFunctionException e) {
                        Log.d(TAG, "onError: ", e);
                        pw.println("Error executing app function: " + e.getErrorCode() + " - " + e);
                        resultCode.set(-1);
                        countDownLatch.countDown();
                    }
                };
@@ -322,94 +400,11 @@ public class AppFunctionManagerServiceShellCommand extends ShellCommand {
        if (!returned) {
            pw.println("Timed out");
            cancellationSignal.cancel();
            resultCode.set(-1);
        }
        pw.flush();

        return 0;
    }

    /**
     * Converts a JSON string to a {@link GenericDocument}.
     *
     * <p>This method parses the provided JSON string and creates a {@link GenericDocument}
     * representation. It extracts the 'id', 'namespace', and 'schemaType' fields from the top-level
     * JSON object to initialize the {@code GenericDocument}. It then iterates through the remaining
     * keys in the JSON object and adds them as properties to the {@code GenericDocument}.
     *
     * <p>Example Input:
     *
     * <pre>{@code
     * {"createNoteParams":{"title":"My title"}}
     * }</pre>
     */
    private static GenericDocument parseJsonToGenericDocument(String jsonString)
            throws JSONException {
        JSONObject json = new JSONObject(jsonString);

        String id = json.optString("id", "");
        String namespace = json.optString("namespace", "");
        String schemaType = json.optString("schemaType", "");

        GenericDocument.Builder builder = new GenericDocument.Builder(id, namespace, schemaType);

        Iterator<String> keys = json.keys();
        while (keys.hasNext()) {
            String key = keys.next();
            Object value = json.get(key);

            if (value instanceof String) {
                builder.setPropertyString(key, (String) value);
            } else if (value instanceof Integer || value instanceof Long) {
                builder.setPropertyLong(key, ((Number) value).longValue());
            } else if (value instanceof Double || value instanceof Float) {
                builder.setPropertyDouble(key, ((Number) value).doubleValue());
            } else if (value instanceof Boolean) {
                builder.setPropertyBoolean(key, (Boolean) value);
            } else if (value instanceof JSONObject) {
                GenericDocument nestedDocument = parseJsonToGenericDocument(value.toString());
                builder.setPropertyDocument(key, nestedDocument);
            } else if (value instanceof JSONArray) {
                JSONArray array = (JSONArray) value;
                if (array.length() == 0) {
                    continue;
                }

                Object first = array.get(0);
                if (first instanceof String) {
                    String[] arr = new String[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        arr[i] = array.optString(i, null);
                    }
                    builder.setPropertyString(key, arr);
                } else if (first instanceof Integer || first instanceof Long) {
                    long[] arr = new long[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        arr[i] = array.getLong(i);
                    }
                    builder.setPropertyLong(key, arr);
                } else if (first instanceof Double || first instanceof Float) {
                    double[] arr = new double[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        arr[i] = array.getDouble(i);
                    }
                    builder.setPropertyDouble(key, arr);
                } else if (first instanceof Boolean) {
                    boolean[] arr = new boolean[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        arr[i] = array.getBoolean(i);
                    }
                    builder.setPropertyBoolean(key, arr);
                } else if (first instanceof JSONObject) {
                    GenericDocument[] documentArray = new GenericDocument[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        documentArray[i] =
                                parseJsonToGenericDocument(array.getJSONObject(i).toString());
                    }
                    builder.setPropertyDocument(key, documentArray);
                }
            }
        }
        return builder.build();
        return resultCode.get();
    }

    private int runGrantAppFunctionAccess() throws Exception {
+207 −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.app.appsearch.GenericDocument;
import android.app.appsearch.SearchResult;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class AppSearchDataJsonConverter {

    private AppSearchDataJsonConverter() {}

    /**
     * Converts a JSON string to a {@link GenericDocument}.
     *
     * <p>This method parses the provided JSON string and creates a {@link GenericDocument}
     * representation. It extracts the 'id', 'namespace', and 'schemaType' fields from the top-level
     * JSON object to initialize the {@code GenericDocument}. It then iterates through the remaining
     * keys in the JSON object and adds them as properties to the {@code GenericDocument}.
     *
     * <p>Example Input:
     *
     * <pre>{@code
     * {"createNoteParams":{"title":"My title"}}
     * }</pre>
     */
    public static GenericDocument convertJsonToGenericDocument(String jsonString)
            throws JSONException {
        JSONObject json = new JSONObject(jsonString);

        String id = json.optString("id", "");
        String namespace = json.optString("namespace", "");
        String schemaType = json.optString("schemaType", "");

        GenericDocument.Builder builder = new GenericDocument.Builder(namespace, id, schemaType);

        Iterator<String> keys = json.keys();
        while (keys.hasNext()) {
            String key = keys.next();
            Object value = json.get(key);

            if (value instanceof String) {
                builder.setPropertyString(key, (String) value);
            } else if (value instanceof Integer || value instanceof Long) {
                builder.setPropertyLong(key, ((Number) value).longValue());
            } else if (value instanceof Double || value instanceof Float) {
                builder.setPropertyDouble(key, ((Number) value).doubleValue());
            } else if (value instanceof Boolean) {
                builder.setPropertyBoolean(key, (Boolean) value);
            } else if (value instanceof JSONObject) {
                GenericDocument nestedDocument = convertJsonToGenericDocument(value.toString());
                builder.setPropertyDocument(key, nestedDocument);
            } else if (value instanceof JSONArray) {
                JSONArray array = (JSONArray) value;
                if (array.length() == 0) {
                    continue;
                }

                Object first = array.get(0);
                if (first instanceof String) {
                    String[] arr = new String[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        arr[i] = array.optString(i, null);
                    }
                    builder.setPropertyString(key, arr);
                } else if (first instanceof Integer || first instanceof Long) {
                    long[] arr = new long[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        arr[i] = array.getLong(i);
                    }
                    builder.setPropertyLong(key, arr);
                } else if (first instanceof Double || first instanceof Float) {
                    double[] arr = new double[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        arr[i] = array.getDouble(i);
                    }
                    builder.setPropertyDouble(key, arr);
                } else if (first instanceof Boolean) {
                    boolean[] arr = new boolean[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        arr[i] = array.getBoolean(i);
                    }
                    builder.setPropertyBoolean(key, arr);
                } else if (first instanceof JSONObject) {
                    GenericDocument[] documentArray = new GenericDocument[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        documentArray[i] =
                                convertJsonToGenericDocument(array.getJSONObject(i).toString());
                    }
                    builder.setPropertyDocument(key, documentArray);
                }
            }
        }
        return builder.build();
    }

    /**
     * Converts an array of {@link GenericDocument} objects into a {@link JSONArray}.
     *
     * @param documents The array of {@link GenericDocument} to convert.
     * @return A {@link JSONArray} where each element is the JSON representation of a {@link
     *     GenericDocument}.
     * @throws JSONException if there is an error during JSON conversion.
     */
    public static JSONArray convertGenericDocumentsToJsonArray(GenericDocument[] documents)
            throws JSONException {
        JSONArray jsonArray = new JSONArray();
        for (GenericDocument doc : documents) {
            jsonArray.put(convertGenericDocumentToJson(doc));
        }
        return jsonArray;
    }

    /**
     * Converts a single {@link GenericDocument} into a {@link JSONObject}.
     *
     * <p>This method iterates over all properties of the given {@code GenericDocument}. All
     * properties, regardless of whether they are single or repeated, are converted into a {@link
     * JSONArray}.
     *
     * @param genericDocument The {@link GenericDocument} to convert.
     * @return The {@link JSONObject} representation of the document.
     * @throws JSONException if there is an error during JSON conversion.
     */
    public static JSONObject convertGenericDocumentToJson(GenericDocument genericDocument)
            throws JSONException {
        JSONObject jsonObject = new JSONObject();
        Set<String> propertyNames = genericDocument.getPropertyNames();

        for (String propertyName : propertyNames) {
            Object propertyValue = genericDocument.getProperty(propertyName);
            if (propertyValue == null) {
                jsonObject.put(propertyName, JSONObject.NULL);
                continue;
            }

            // HACK: GenericDocument doesn't tell whether a property is singular or repeated.
            // Here, we always convert a property into an array.
            if (propertyValue instanceof GenericDocument[]) {
                GenericDocument[] documentValues = (GenericDocument[]) propertyValue;
                JSONArray jsonArray = new JSONArray();
                for (GenericDocument doc : documentValues) {
                    jsonArray.put(convertGenericDocumentToJson(doc));
                }
                jsonObject.put(propertyName, jsonArray);
            } else if (propertyValue.getClass().isArray()) {
                JSONArray jsonArray = new JSONArray();
                int propertyArrLength = Array.getLength(propertyValue);
                for (int i = 0; i < propertyArrLength; i++) {
                    Object propertyElement = Array.get(propertyValue, i);
                    jsonArray.put(propertyElement);
                }
                jsonObject.put(propertyName, jsonArray);
            }
        }
        return jsonObject;
    }

    /**
     * Converts a {@link SearchResult}, including any nested joined results, into a single {@link
     * JSONObject}.
     *
     * <p>The resulting JSON object uses the schema type of each document as the key for its JSON
     * representation.
     *
     * @param searchResult The {@link SearchResult} to convert.
     * @return A {@link JSONObject} representing the nested structure of the search result.
     * @throws JSONException if there is an error during JSON conversion.
     */
    public static JSONObject searchResultToJsonObject(SearchResult searchResult)
            throws JSONException {
        JSONObject jsonObject = new JSONObject();
        GenericDocument genericDocument = searchResult.getGenericDocument();
        jsonObject.put(
                genericDocument.getSchemaType(),
                AppSearchDataJsonConverter.convertGenericDocumentToJson(genericDocument));

        List<SearchResult> joinedResults = searchResult.getJoinedResults();
        for (SearchResult joinedResult : joinedResults) {
            jsonObject.put(
                    joinedResult.getGenericDocument().getSchemaType(),
                    searchResultToJsonObject(joinedResult));
        }
        return jsonObject;
    }
}
+263 −0

File added.

Preview size limit exceeded, changes collapsed.