content: package com.termux.shared.shell.am;
  
  import android.Manifest;
  import android.app.Application;
  import android.content.Context;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  
  import com.termux.am.Am;
  import com.termux.shared.R;
  import com.termux.shared.android.PackageUtils;
  import com.termux.shared.android.PermissionUtils;
  import com.termux.shared.errors.Error;
  import com.termux.shared.logger.Logger;
  import com.termux.shared.net.socket.local.ILocalSocketManager;
  import com.termux.shared.net.socket.local.LocalClientSocket;
  import com.termux.shared.net.socket.local.LocalServerSocket;
  import com.termux.shared.net.socket.local.LocalSocketManager;
  import com.termux.shared.net.socket.local.LocalSocketManagerClientBase;
  import com.termux.shared.net.socket.local.LocalSocketRunConfig;
  import com.termux.shared.shell.ArgumentTokenizer;
  import com.termux.shared.shell.command.ExecutionCommand;
  
  import java.io.ByteArrayOutputStream;
  import java.io.PrintStream;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  
  /**
   * A AF_UNIX/SOCK_STREAM local server managed with {@link LocalSocketManager} whose
   * {@link LocalServerSocket} receives android activity manager (am) commands from {@link LocalClientSocket}
   * and runs them with termux-am-library. It would normally only allow processes belonging to the
   * server app's user and root user to connect to it.
   *
   * The client must send the am command as a string without the initial "am" arg on its output stream
   * and then wait for the result on its input stream. The result of the execution or error is sent
   * back in the format `exit_code\0stdout\0stderr\0` where `\0` represents a null character.
   * Check termux/termux-am-socket for implementation of a native c client.
   *
   * Usage:
   * 1. Optionally extend {@link AmSocketServerClient}, the implementation for
   *    {@link ILocalSocketManager} that will receive call backs from the server including
   *    when client connects via {@link ILocalSocketManager#onClientAccepted(LocalSocketManager, LocalClientSocket)}.
   * 2. Create a {@link AmSocketServerRunConfig} instance which extends from {@link LocalSocketRunConfig}
   *    with the run config of the am server. It would  be better to use a filesystem socket instead
   *    of abstract namespace socket for security reasons.
   * 3. Call {@link #start(Context, LocalSocketRunConfig)} to start the server and store the {@link LocalSocketManager}
   *    instance returned.
   * 4. Stop server if needed with a call to {@link LocalSocketManager#stop()} on the
   *    {@link LocalSocketManager} instance returned by start call.
   *
   * https://github.com/termux/termux-am-library/blob/main/termux-am-library/src/main/java/com/termux/am/Am.java
   * https://github.com/termux/termux-am-socket
   * https://developer.android.com/studio/command-line/adb#am
   * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
   */
  public class AmSocketServer {
  
      public static final String LOG_TAG = "AmSocketServer";
  
      /**
       * Create the {@link AmSocketServer} {@link LocalServerSocket} and start listening for new {@link LocalClientSocket}.
       *
       * @param context The {@link Context} for {@link LocalSocketManager}.
       * @param localSocketRunConfig The {@link LocalSocketRunConfig} for {@link LocalSocketManager}.
       */
      public static synchronized LocalSocketManager start(@NonNull Context context,
                                                          @NonNull LocalSocketRunConfig localSocketRunConfig) {
          LocalSocketManager localSocketManager = new LocalSocketManager(context, localSocketRunConfig);
          Error error = localSocketManager.start();
          if (error != null) {
              localSocketManager.onError(error);
              return null;
          }
  
          return localSocketManager;
      }
  
      public static void processAmClient(@NonNull LocalSocketManager localSocketManager,
                                         @NonNull LocalClientSocket clientSocket) {
          Error error;
  
          // Read amCommandString client sent and close input stream
          StringBuilder data = new StringBuilder();
          error = clientSocket.readDataOnInputStream(data, true);
          if (error != null) {
              sendResultToClient(localSocketManager, clientSocket, 1, null, error.toString());
              return;
          }
  
          String amCommandString = data.toString();
  
          Logger.logVerbose(LOG_TAG, "am command received from peer " + clientSocket.getPeerCred().getMinimalString() +
              "\nam command: `" + amCommandString + "`");
  
          // Parse am command string and convert it to a list of arguments
          List<String> amCommandList = new ArrayList<>();
          error = parseAmCommand(amCommandString, amCommandList);
          if (error != null) {
              sendResultToClient(localSocketManager, clientSocket, 1, null, error.toString());
              return;
          }
  
          String[] amCommandArray = amCommandList.toArray(new String[0]);
  
          Logger.logDebug(LOG_TAG, "am command received from peer " + clientSocket.getPeerCred().getMinimalString() +
              "\n" + ExecutionCommand.getArgumentsLogString("am command", amCommandArray));
  
          AmSocketServerRunConfig amSocketServerRunConfig = (AmSocketServerRunConfig) localSocketManager.getLocalSocketRunConfig();
  
          // Run am command and send its result to the client
          StringBuilder stdout = new StringBuilder();
          StringBuilder stderr = new StringBuilder();
          error = runAmCommand(localSocketManager.getContext(), amCommandArray, stdout, stderr,
              amSocketServerRunConfig.shouldCheckDisplayOverAppsPermission());
          if (error != null) {
              sendResultToClient(localSocketManager, clientSocket, 1, stdout.toString(),
                  !stderr.toString().isEmpty() ? stderr + "\n\n" + error : error.toString());
          }
  
          sendResultToClient(localSocketManager, clientSocket, 0, stdout.toString(), stderr.toString());
      }
  
      /**
       * Send result to {@link LocalClientSocket} that requested the am command to be run.
       *
       * @param localSocketManager The {@link LocalSocketManager} instance for the local socket.
       * @param clientSocket The {@link LocalClientSocket} to which the result is to be sent.
       * @param exitCode The exit code value to send.
       * @param stdout The stdout value to send.
       * @param stderr The stderr value to send.
       */
      public static void sendResultToClient(@NonNull LocalSocketManager localSocketManager,
                                            @NonNull LocalClientSocket clientSocket,
                                            int exitCode,
                                            @Nullable String stdout, @Nullable String stderr) {
          StringBuilder result = new StringBuilder();
          result.append(sanitizeExitCode(clientSocket, exitCode));
          result.append('\0');
          result.append(stdout != null ? stdout : "");
          result.append('\0');
          result.append(stderr != null ? stderr : "");
  
          // Send result to client and close output stream
          Error error = clientSocket.sendDataToOutputStream(result.toString(), true);
          if (error != null) {
              localSocketManager.onError(clientSocket, error);
          }
      }
  
      /**
       * Sanitize exitCode to between 0-255, otherwise it may be considered invalid.
       * Out of bound exit codes would return with exit code `44` `Channel number out of range` in shell.
       *
       * @param clientSocket The {@link LocalClientSocket} to which the exit code will be sent.
       * @param exitCode The current exit code.
       * @return Returns the sanitized exit code.
       */
      public static int sanitizeExitCode(@NonNull LocalClientSocket clientSocket, int exitCode) {
          if (exitCode < 0 || exitCode > 255) {
              Logger.logWarn(LOG_TAG, "Ignoring invalid peer "  + clientSocket.getPeerCred().getMinimalString() + " result value \"" + exitCode + "\" and force setting it to \"" + 1 + "\"");
              exitCode = 1;
          }
  
          return exitCode;
      }
  
  
      /**
       * Parse amCommandString into a list of arguments like normally done on shells like bourne shell.
       * Arguments are split on whitespaces unless quoted with single or double quotes.
       * Double quotes and backslashes can be escaped with backslashes in arguments surrounded.
       * Double quotes and backslashes can be escaped with backslashes in arguments surrounded with
       * double quotes.
       *
       * @param amCommandString The am command {@link String}.
       * @param amCommandList The {@link List<String>} to set list of arguments in.
       * @return Returns the {@code error} if parsing am command failed, otherwise {@code null}.
       */
      public static Error parseAmCommand(String amCommandString, List<String> amCommandList) {
  
          if (amCommandString == null || amCommandString.isEmpty()) {
              return null;
          }
  
          try {
              amCommandList.addAll(ArgumentTokenizer.tokenize(amCommandString));
          } catch (Exception e) {
              return AmSocketServerErrno.ERRNO_PARSE_AM_COMMAND_FAILED_WITH_EXCEPTION.getError(e, amCommandString, e.getMessage());
          }
  
          return null;
      }
  
      /**
       * Call termux-am-library to run the am command.
       *
       * @param context The {@link Context} to run am command with.
       * @param amCommandArray The am command array.
       * @param stdout The {@link StringBuilder} to set stdout in that is returned by the am command.
       * @param stderr The {@link StringBuilder} to set stderr in that is returned by the am command.
       * @param checkDisplayOverAppsPermission Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW}
       *                                       has been granted if running on Android `>= 10` and
       *                                       starting activity or service.
       * @return Returns the {@code error} if am command failed, otherwise {@code null}.
       */
      public static Error runAmCommand(@NonNull Context context,
                                       String[] amCommandArray,
                                       @NonNull StringBuilder stdout, @NonNull StringBuilder stderr,
                                       boolean checkDisplayOverAppsPermission) {
          try (ByteArrayOutputStream stdoutByteStream = new ByteArrayOutputStream();
               PrintStream stdoutPrintStream = new PrintStream(stdoutByteStream);
               ByteArrayOutputStream stderrByteStream = new ByteArrayOutputStream();
               PrintStream stderrPrintStream = new PrintStream(stderrByteStream)) {
  
              if (checkDisplayOverAppsPermission && amCommandArray.length >= 1 &&
                  (amCommandArray[0].equals("start") || amCommandArray[0].equals("startservice")) &&
                  !PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(context, true)) {
                  throw new IllegalStateException(context.getString(R.string.error_display_over_other_apps_permission_not_granted,
                      PackageUtils.getAppNameForPackage(context)));
              }
  
              new Am(stdoutPrintStream, stderrPrintStream, (Application) context.getApplicationContext()).run(amCommandArray);
  
              // Set stdout to value set by am command in stdoutPrintStream
              stdoutPrintStream.flush();
              stdout.append(stdoutByteStream.toString(StandardCharsets.UTF_8.name()));
  
              // Set stderr to value set by am command in stderrPrintStream
              stderrPrintStream.flush();
              stderr.append(stderrByteStream.toString(StandardCharsets.UTF_8.name()));
          } catch (Exception e) {
              return AmSocketServerErrno.ERRNO_RUN_AM_COMMAND_FAILED_WITH_EXCEPTION.getError(e, Arrays.toString(amCommandArray), e.getMessage());
          }
  
          return null;
      }
  
  
  
  
  
      /** Implementation for {@link ILocalSocketManager} for {@link AmSocketServer}. */
      public abstract static class AmSocketServerClient extends LocalSocketManagerClientBase {
  
          @Override
          public void onClientAccepted(@NonNull LocalSocketManager localSocketManager,
                                       @NonNull LocalClientSocket clientSocket) {
              AmSocketServer.processAmClient(localSocketManager, clientSocket);
              super.onClientAccepted(localSocketManager, clientSocket);
          }
  
      }
  
  }
  