content: package com.genymobile.scrcpy;
  
  import android.os.BatteryManager;
  import android.os.Build;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  
  public final class Server {
  
      public static final String SERVER_PATH;
  
      static {
          String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator);
          // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath
          SERVER_PATH = classPaths[0];
      }
  
      private static class Completion {
          private int running;
          private boolean fatalError;
  
          Completion(int running) {
              this.running = running;
          }
  
          synchronized void addCompleted(boolean fatalError) {
              --running;
              if (fatalError) {
                  this.fatalError = true;
              }
              if (running == 0 || this.fatalError) {
                  notify();
              }
          }
  
          synchronized void await() {
              try {
                  while (running > 0 && !fatalError) {
                      wait();
                  }
              } catch (InterruptedException e) {
                  // ignore
              }
          }
      }
  
      private Server() {
          // not instantiable
      }
  
      private static void initAndCleanUp(Options options, CleanUp cleanUp) {
          // This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
          // and for all, they cannot be changed from another thread)
  
          if (options.getShowTouches()) {
              try {
                  String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
                  // If "show touches" was disabled, it must be disabled back on clean up
                  if (!"1".equals(oldValue)) {
                      if (!cleanUp.setDisableShowTouches(true)) {
                          Ln.e("Could not disable show touch on exit");
                      }
                  }
              } catch (SettingsException e) {
                  Ln.e("Could not change \"show_touches\"", e);
              }
          }
  
          if (options.getStayAwake()) {
              int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
              try {
                  String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
                  try {
                      int restoreStayOn = Integer.parseInt(oldValue);
                      if (restoreStayOn != stayOn) {
                          // Restore only if the current value is different
                          if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
                              Ln.e("Could not restore stay on on exit");
                          }
                      }
                  } catch (NumberFormatException e) {
                      // ignore
                  }
              } catch (SettingsException e) {
                  Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
              }
          }
  
          if (options.getPowerOffScreenOnClose()) {
              if (!cleanUp.setPowerOffScreen(true)) {
                  Ln.e("Could not power off screen on exit");
              }
          }
      }
  
      private static void scrcpy(Options options) throws IOException, ConfigurationException {
          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) {
              Ln.e("Camera mirroring is not supported before Android 12");
              throw new ConfigurationException("Camera mirroring is not supported");
          }
  
          CleanUp cleanUp = null;
          Thread initThread = null;
  
          if (options.getCleanup()) {
              cleanUp = CleanUp.configure(options.getDisplayId());
              initThread = startInitThread(options, cleanUp);
          }
  
          int scid = options.getScid();
          boolean tunnelForward = options.isTunnelForward();
          boolean control = options.getControl();
          boolean video = options.getVideo();
          boolean audio = options.getAudio();
          boolean sendDummyByte = options.getSendDummyByte();
          boolean camera = video && options.getVideoSource() == VideoSource.CAMERA;
  
          final Device device = camera ? null : new Device(options);
  
          Workarounds.apply(audio, camera);
  
          List<AsyncProcessor> asyncProcessors = new ArrayList<>();
  
          DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte);
          try {
              if (options.getSendDeviceMeta()) {
                  connection.sendDeviceMeta(Device.getDeviceName());
              }
  
              if (control) {
                  ControlChannel controlChannel = connection.getControlChannel();
                  Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
  
                  // scrcpy-mask: send device size and rotation when connected
                  controller.getSender().send(DeviceMessage.createRotation(
                          device.smOriginalRotation,
                          device.getDeviceSizeWithRotation(device.smOriginalRotation))
                  );
  
                  // scrcpy-mask: add RotationListener for control socket
                  device.smRotationListener = (rotation -> {
                      Size size = device.getDeviceSizeWithRotation(rotation);
                      DeviceMessage msg = DeviceMessage.createRotation(rotation, size);
                      controller.getSender().send(msg);
                  });
  
                  device.setClipboardListener(text -> {
                      DeviceMessage msg = DeviceMessage.createClipboard(text);
                      controller.getSender().send(msg);
                  });
                  asyncProcessors.add(controller);
              }
  
              if (audio) {
                  AudioCodec audioCodec = options.getAudioCodec();
                  AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
                  Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
                  AsyncProcessor audioRecorder;
                  if (audioCodec == AudioCodec.RAW) {
                      audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
                  } else {
                      audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
                              options.getAudioEncoder());
                  }
                  asyncProcessors.add(audioRecorder);
              }
  
              if (video) {
                  Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
                          options.getSendFrameMeta());
                  SurfaceCapture surfaceCapture;
                  if (options.getVideoSource() == VideoSource.DISPLAY) {
                      surfaceCapture = new ScreenCapture(device);
                  } else {
                      surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
                              options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
                  }
                  SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
                          options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
                  asyncProcessors.add(surfaceEncoder);
              }
  
              Completion completion = new Completion(asyncProcessors.size());
              for (AsyncProcessor asyncProcessor : asyncProcessors) {
                  asyncProcessor.start((fatalError) -> {
                      completion.addCompleted(fatalError);
                  });
              }
  
              completion.await();
          } finally {
              if (initThread != null) {
                  initThread.interrupt();
              }
              for (AsyncProcessor asyncProcessor : asyncProcessors) {
                  asyncProcessor.stop();
              }
  
              connection.shutdown();
  
              try {
                  if (initThread != null) {
                      initThread.join();
                  }
                  for (AsyncProcessor asyncProcessor : asyncProcessors) {
                      asyncProcessor.join();
                  }
              } catch (InterruptedException e) {
                  // ignore
              }
  
              connection.close();
          }
      }
  
      private static Thread startInitThread(final Options options, final CleanUp cleanUp) {
          Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup");
          thread.start();
          return thread;
      }
  
      public static void main(String... args) {
          int status = 0;
          try {
              internalMain(args);
          } catch (Throwable t) {
              Ln.e(t.getMessage(), t);
              status = 1;
          } finally {
              // By default, the Java process exits when all non-daemon threads are terminated.
              // The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit.
              // So force the process to exit explicitly.
              System.exit(status);
          }
      }
  
      private static void internalMain(String... args) throws Exception {
          Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
              Ln.e("Exception on thread " + t, e);
          });
  
          Options options = Options.parse(args);
  
          Ln.disableSystemStreams();
          Ln.initLogLevel(options.getLogLevel());
  
          Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
  
          if (options.getList()) {
              if (options.getCleanup()) {
                  CleanUp.unlinkSelf();
              }
  
              if (options.getListEncoders()) {
                  Ln.i(LogUtils.buildVideoEncoderListMessage());
                  Ln.i(LogUtils.buildAudioEncoderListMessage());
              }
              if (options.getListDisplays()) {
                  Ln.i(LogUtils.buildDisplayListMessage());
              }
              if (options.getListCameras() || options.getListCameraSizes()) {
                  Workarounds.apply(false, true);
                  Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
              }
              // Just print the requested data, do not mirror
              return;
          }
  
          try {
              scrcpy(options);
          } catch (ConfigurationException e) {
              // Do not print stack trace, a user-friendly error-message has already been logged
          }
      }
  }
  