content: package com.termux.app;
  
  import android.app.Activity;
  import android.app.AlertDialog;
  import android.app.ProgressDialog;
  import android.content.Context;
  import android.os.Build;
  import android.os.Environment;
  import android.system.Os;
  import android.util.Pair;
  import android.view.WindowManager;
  
  import com.termux.R;
  import com.termux.shared.file.FileUtils;
  import com.termux.shared.termux.crash.TermuxCrashUtils;
  import com.termux.shared.termux.file.TermuxFileUtils;
  import com.termux.shared.interact.MessageDialogUtils;
  import com.termux.shared.logger.Logger;
  import com.termux.shared.markdown.MarkdownUtils;
  import com.termux.shared.errors.Error;
  import com.termux.shared.android.PackageUtils;
  import com.termux.shared.termux.TermuxConstants;
  import com.termux.shared.termux.TermuxUtils;
  import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
  
  import java.io.BufferedReader;
  import java.io.ByteArrayInputStream;
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.InputStreamReader;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipInputStream;
  
  import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR;
  import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH;
  import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR;
  import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
  
  /**
   * Install the Termux bootstrap packages if necessary by following the below steps:
   * <p/>
   * (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
   * broken $PREFIX directory below.
   * <p/>
   * (2) A progress dialog is shown with "Installing..." message and a spinner.
   * <p/>
   * (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below.
   * <p/>
   * (4) The zip file is loaded from a shared library.
   * <p/>
   * (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
   * continuously encountering zip file entries:
   * <p/>
   * (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
   * <p/>
   * (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
   */
  final class TermuxInstaller {
  
      private static final String LOG_TAG = "TermuxInstaller";
  
      /** Performs bootstrap setup if necessary. */
      static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
          String bootstrapErrorMessage;
          Error filesDirectoryAccessibleError;
  
          // This will also call Context.getFilesDir(), which should ensure that termux files directory
          // is created if it does not already exist
          filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
          boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
  
          // Termux can only be run as the primary user (device owner) since only that
          // account has the expected file system paths. Verify that:
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PackageUtils.isCurrentUserThePrimaryUser(activity)) {
              bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message,
                  MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
              Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
              Logger.logError(LOG_TAG, bootstrapErrorMessage);
              sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
              MessageDialogUtils.exitAppWithErrorMessage(activity,
                  activity.getString(R.string.bootstrap_error_title),
                  bootstrapErrorMessage);
              return;
          }
  
          if (!isFilesDirectoryAccessible) {
              bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError);
              //noinspection SdCardPath
              if (PackageUtils.isAppInstalledOnExternalStorage(activity) &&
                  !TermuxConstants.TERMUX_FILES_DIR_PATH.equals(activity.getFilesDir().getAbsolutePath().replaceAll("^/data/user/0/", "/data/data/"))) {
                  bootstrapErrorMessage += "\n\n" + activity.getString(R.string.bootstrap_error_installed_on_portable_sd,
                      MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
              }
  
              Logger.logError(LOG_TAG, bootstrapErrorMessage);
              sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
              MessageDialogUtils.showMessage(activity,
                  activity.getString(R.string.bootstrap_error_title),
                  bootstrapErrorMessage, null);
              return;
          }
  
          // If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
          if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
              if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) {
                  Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains specific unimportant files.");
              } else {
                  whenDone.run();
                  return;
              }
          } else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) {
              Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" does not exist but another file exists at its destination.");
          }
  
          final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
          new Thread() {
              @Override
              public void run() {
                  try {
                      Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
  
                      Error error;
  
                      // Delete prefix staging directory or any file at its destination
                      error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
                      if (error != null) {
                          showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
                          return;
                      }
  
                      // Delete prefix directory or any file at its destination
                      error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
                      if (error != null) {
                          showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
                          return;
                      }
  
                      // Create prefix staging directory if it does not already exist and set required permissions
                      error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true);
                      if (error != null) {
                          showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
                          return;
                      }
  
                      // Create prefix directory if it does not already exist and set required permissions
                      error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true);
                      if (error != null) {
                          showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
                          return;
                      }
  
                      Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
  
                      final byte[] buffer = new byte[8096];
                      final List<Pair<String, String>> symlinks = new ArrayList<>(50);
  
                      final byte[] zipBytes = loadZipBytes();
                      try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
                          ZipEntry zipEntry;
                          while ((zipEntry = zipInput.getNextEntry()) != null) {
                              if (zipEntry.getName().equals("SYMLINKS.txt")) {
                                  BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
                                  String line;
                                  while ((line = symlinksReader.readLine()) != null) {
                                      String[] parts = line.split("←");
                                      if (parts.length != 2)
                                          throw new RuntimeException("Malformed symlink line: " + line);
                                      String oldPath = parts[0];
                                      String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1];
                                      symlinks.add(Pair.create(oldPath, newPath));
  
                                      error = ensureDirectoryExists(new File(newPath).getParentFile());
                                      if (error != null) {
                                          showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
                                          return;
                                      }
                                  }
                              } else {
                                  String zipEntryName = zipEntry.getName();
                                  File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);
                                  boolean isDirectory = zipEntry.isDirectory();
  
                                  error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
                                  if (error != null) {
                                      showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
                                      return;
                                  }
  
                                  if (!isDirectory) {
                                      try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
                                          int readBytes;
                                          while ((readBytes = zipInput.read(buffer)) != -1)
                                              outStream.write(buffer, 0, readBytes);
                                      }
                                      if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
                                          zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods")) {
                                          //noinspection OctalInteger
                                          Os.chmod(targetFile.getAbsolutePath(), 0700);
                                      }
                                  }
                              }
                          }
                      }
  
                      if (symlinks.isEmpty())
                          throw new RuntimeException("No SYMLINKS.txt encountered");
                      for (Pair<String, String> symlink : symlinks) {
                          Os.symlink(symlink.first, symlink.second);
                      }
  
                      Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory.");
  
                      if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) {
                          throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
                      }
  
                      Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
  
                      // Recreate env file since termux prefix was wiped earlier
                      TermuxShellEnvironment.writeEnvironmentToFile(activity);
  
                      activity.runOnUiThread(whenDone);
  
                  } catch (final Exception e) {
                      showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
  
                  } finally {
                      activity.runOnUiThread(() -> {
                          try {
                              progress.dismiss();
                          } catch (RuntimeException e) {
                              // Activity already dismissed - ignore.
                          }
                      });
                  }
              }
          }.start();
      }
  
      public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
          Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
  
          // Send a notification with the exception so that the user knows why bootstrap setup failed
          sendBootstrapCrashReportNotification(activity, message);
  
          activity.runOnUiThread(() -> {
              try {
                  new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
                      .setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
                          dialog.dismiss();
                          activity.finish();
                      })
                      .setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
                          dialog.dismiss();
                          FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
                          TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
                      }).show();
              } catch (WindowManager.BadTokenException e1) {
                  // Activity already dismissed - ignore.
              }
          });
      }
  
      private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
          final String title = TermuxConstants.TERMUX_APP_NAME + " Bootstrap Error";
  
          // Add info of all install Termux plugin apps as well since their target sdk or installation
          // on external/portable sd card can affect Termux app files directory access or exec.
          TermuxCrashUtils.sendCrashReportNotification(activity, LOG_TAG,
              title, null, "## " + title + "\n\n" + message + "\n\n" +
                  TermuxUtils.getTermuxDebugMarkdownString(activity),
              true, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES, true);
      }
  
      static void setupStorageSymlinks(final Context context) {
          final String LOG_TAG = "termux-storage";
          final String title = TermuxConstants.TERMUX_APP_NAME + " Setup Storage Error";
  
          Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
  
          new Thread() {
              public void run() {
                  try {
                      Error error;
                      File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
  
                      error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
                      if (error != null) {
                          Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
                          Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
                          TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
                              "## " + title + "\n\n" + Error.getErrorMarkdownString(error),
                              true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
                          return;
                      }
  
                      Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
  
                      // Get primary storage root "/storage/emulated/0" symlink
                      File sharedDir = Environment.getExternalStorageDirectory();
                      Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
  
                      File documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
                      Os.symlink(documentsDir.getAbsolutePath(), new File(storageDir, "documents").getAbsolutePath());
  
                      File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                      Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
  
                      File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
                      Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath());
  
                      File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
                      Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath());
  
                      File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
                      Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath());
  
                      File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
                      Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
  
                      File podcastsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PODCASTS);
                      Os.symlink(podcastsDir.getAbsolutePath(), new File(storageDir, "podcasts").getAbsolutePath());
  
                      if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
                          File audiobooksDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_AUDIOBOOKS);
                          Os.symlink(audiobooksDir.getAbsolutePath(), new File(storageDir, "audiobooks").getAbsolutePath());
                      }
  
                      // Dir 0 should ideally be for primary storage
                      // https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ContextImpl.java;l=818
                      // https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java;l=219
                      // https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java;l=181
                      // https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/StorageManagerService.java;l=3796
                      // https://cs.android.com/android/platform/superproject/+/android-7.0.0_r36:frameworks/base/services/core/java/com/android/server/MountService.java;l=3053
  
                      // Create "Android/data/com.termux" symlinks
                      File[] dirs = context.getExternalFilesDirs(null);
                      if (dirs != null && dirs.length > 0) {
                          for (int i = 0; i < dirs.length; i++) {
                              File dir = dirs[i];
                              if (dir == null) continue;
                              String symlinkName = "external-" + i;
                              Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
                              Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
                          }
                      }
  
                      // Create "Android/media/com.termux" symlinks
                      dirs = context.getExternalMediaDirs();
                      if (dirs != null && dirs.length > 0) {
                          for (int i = 0; i < dirs.length; i++) {
                              File dir = dirs[i];
                              if (dir == null) continue;
                              String symlinkName = "media-" + i;
                              Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
                              Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
                          }
                      }
  
                      Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
                  } catch (Exception e) {
                      Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
                      Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
                      TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
                          "## " + title + "\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)),
                          true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
                  }
              }
          }.start();
      }
  
      private static Error ensureDirectoryExists(File directory) {
          return FileUtils.createDirectoryFile(directory.getAbsolutePath());
      }
  
      public static byte[] loadZipBytes() {
          // Only load the shared library when necessary to save memory usage.
          System.loadLibrary("termux-bootstrap");
          return getZip();
      }
  
      public static native byte[] getZip();
  
  }
  