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

Commit f6ab114d authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add a shell command to execute an app function" into main

parents adbb019a 49b2fd13
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -63,7 +63,10 @@ import android.os.IBinder;
import android.os.ICancellationSignal;
import android.os.OutcomeReceiver;
import android.os.ParcelableException;
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Slog;
@@ -156,6 +159,14 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
        }
    }

    @Override
    public void onShellCommand(FileDescriptor in, FileDescriptor out,
            FileDescriptor err, @NonNull String[] args, ShellCallback callback,
            @NonNull ResultReceiver resultReceiver) {
        new AppFunctionManagerServiceShellCommand(this)
                .exec(this, in, out, err, args, callback, resultReceiver);
    }

    @Override
    public ICancellationSignal executeAppFunction(
            @NonNull ExecuteAppFunctionAidlRequest requestInternal,
@@ -506,6 +517,10 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
        Objects.requireNonNull(packageName);
        Objects.requireNonNull(targetUser);

        if (uid == Process.ROOT_UID) {
            // root is not a package. It does not have a signing info.
            return new SigningInfo();
        }
        PackageInfo packageInfo;
        packageInfo =
                Objects.requireNonNull(
+277 −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.app.ActivityManager;
import android.app.appfunctions.AppFunctionException;
import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
import android.app.appfunctions.ExecuteAppFunctionRequest;
import android.app.appfunctions.ExecuteAppFunctionResponse;
import android.app.appfunctions.IAppFunctionManager;
import android.app.appfunctions.IExecuteAppFunctionCallback;
import android.app.appsearch.GenericDocument;
import android.os.Binder;
import android.os.ICancellationSignal;
import android.os.Process;
import android.os.ShellCommand;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;

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

import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/** Shell command implementation for the {@link AppFunctionManagerService}. */
public class AppFunctionManagerServiceShellCommand extends ShellCommand {

    @NonNull private final IAppFunctionManager mService;

    AppFunctionManagerServiceShellCommand(@NonNull IAppFunctionManager service) {
        mService = Objects.requireNonNull(service);
    }

    @Override
    public void onHelp() {
        final PrintWriter pw = getOutPrintWriter();
        pw.println("AppFunctionManagerService commands:");
        pw.println("  help");
        pw.println("    Prints this help text.");
        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.");
        pw.println("    --package <PACKAGE_NAME>: The target package name.");
        pw.println("    --function <FUNCTION_ID>: The ID of the app function to execute.");
        pw.println(
                "    --parameters <PARAMETERS_JSON>: JSON string containing the parameters for "
                        + "the function.");
        pw.println(
                "    --user <USER_ID> (optional): The user ID to execute the function under. "
                        + "Defaults to the current user.");
        pw.println();
    }

    @Override
    public int onCommand(String cmd) {
        if (cmd == null) {
            return handleDefaultCommands(null);
        }

        try {
            switch (cmd) {
                case "execute-app-function":
                    return runExecuteAppFunction();
                default:
                    return handleDefaultCommands(cmd);
            }
        } catch (Exception e) {
            getOutPrintWriter().println("Exception: " + e);
        }
        return -1;
    }

    private int runExecuteAppFunction() throws Exception {
        final PrintWriter pw = getOutPrintWriter();
        String packageName = null;
        String functionId = null;
        String parametersJson = null;
        int userId = ActivityManager.getCurrentUser();
        String opt;

        while ((opt = getNextOption()) != null) {
            switch (opt) {
                case "--package":
                    packageName = getNextArgRequired();
                    break;
                case "--function":
                    functionId = getNextArgRequired();
                    break;
                case "--parameters":
                    parametersJson = getNextArgRequired();
                    break;
                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;
            }
        }

        if (packageName == null) {
            pw.println("Error: --package must be specified.");
            return -1;
        }
        if (functionId == null) {
            pw.println("Error: --function must be specified.");
            return -1;
        }
        if (parametersJson == null) {
            pw.println("Error: --parameters must be specified.");
            return -1;
        }

        GenericDocument parameters = parseJsonToGenericDocument(parametersJson);

        ExecuteAppFunctionAidlRequest request =
                new ExecuteAppFunctionAidlRequest(
                        new ExecuteAppFunctionRequest.Builder(packageName, functionId)
                                .setParameters(parameters)
                                .build(),
                        UserHandle.of(userId),
                        getCallingPackage(),
                        SystemClock.elapsedRealtime());

        CountDownLatch countDownLatch = new CountDownLatch(1);

        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());
                        countDownLatch.countDown();
                    }

                    @Override
                    public void onError(AppFunctionException e) {
                        Log.d(TAG, "onError: ", e);
                        pw.println("Error executing app function: " + e.getErrorCode() + " - " + e);
                        countDownLatch.countDown();
                    }
                };

        ICancellationSignal cancellationSignal = mService.executeAppFunction(request, callback);

        boolean returned = countDownLatch.await(10, TimeUnit.SECONDS);
        if (!returned) {
            pw.println("Timed out");
            cancellationSignal.cancel();
        }
        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();
    }

    private static String getCallingPackage() {
        return switch (Binder.getCallingUid()) {
            case Process.ROOT_UID -> "root";
            case Process.SHELL_UID -> "com.android.shell";
            default -> throw new IllegalAccessError("Only allow shell or root");
        };
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -169,6 +169,10 @@ class CallerValidatorImpl implements CallerValidator {
     */
    private void validateCallingPackageInternal(
            int actualCallingUid, @NonNull String claimedCallingPackage) {
        if (actualCallingUid == Process.ROOT_UID) {
            // root does not have a package name.
            return;
        }
        UserHandle callingUserHandle = UserHandle.getUserHandleForUid(actualCallingUid);
        Context actualCallingUserContext =
                mContext.createContextAsUser(callingUserHandle, /* flags= */ 0);