content: package org.adaway.model.root;
  
  import static android.content.Context.MODE_PRIVATE;
  import static org.adaway.db.entity.ListType.REDIRECTED;
  import static org.adaway.model.adblocking.AdBlockMethod.ROOT;
  import static org.adaway.model.error.HostError.COPY_FAIL;
  import static org.adaway.model.error.HostError.NOT_ENOUGH_SPACE;
  import static org.adaway.model.error.HostError.PRIVATE_FILE_FAILED;
  import static org.adaway.model.error.HostError.REVERT_FAIL;
  import static org.adaway.model.root.ShellUtils.isWritable;
  import static org.adaway.util.Constants.ANDROID_SYSTEM_ETC_HOSTS;
  import static org.adaway.util.Constants.COMMAND_CHMOD_644;
  import static org.adaway.util.Constants.COMMAND_CHOWN;
  import static org.adaway.util.Constants.DEFAULT_HOSTS_FILENAME;
  import static org.adaway.util.Constants.HOSTS_FILENAME;
  import static org.adaway.util.Constants.LINE_SEPARATOR;
  import static org.adaway.util.Constants.LOCALHOST_HOSTNAME;
  import static org.adaway.util.Constants.LOCALHOST_IPV4;
  import static org.adaway.util.Constants.LOCALHOST_IPV6;
  import static org.adaway.model.root.MountType.READ_ONLY;
  import static org.adaway.model.root.MountType.READ_WRITE;
  import static org.adaway.model.root.ShellUtils.mergeAllLines;
  
  import android.content.Context;
  
  import com.topjohnwu.superuser.Shell;
  
  import org.adaway.R;
  import org.adaway.db.AppDatabase;
  import org.adaway.db.dao.HostEntryDao;
  import org.adaway.db.dao.HostsSourceDao;
  import org.adaway.db.entity.HostEntry;
  import org.adaway.db.entity.HostsSource;
  import org.adaway.helper.PreferenceHelper;
  import org.adaway.model.adblocking.AdBlockMethod;
  import org.adaway.model.adblocking.AdBlockModel;
  import org.adaway.model.error.HostErrorException;
  import org.adaway.util.AppExecutors;
  import org.adaway.util.WebServerUtils;
  
  import java.io.BufferedWriter;
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.OutputStreamWriter;
  import java.text.SimpleDateFormat;
  import java.util.Date;
  import java.util.List;
  import java.util.Locale;
  import java.util.concurrent.Executor;
  
  import timber.log.Timber;
  
  /**
   * This class is the model to represent hosts file installation.
   *
   * @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
   */
  public class RootModel extends AdBlockModel {
      private static final String HEADER1 = "# This hosts file has been generated by AdAway on: ";
      private static final String HEADER2 = "# Please do not modify it directly, it will be overwritten when AdAway is applied again.";
      private static final String HEADER_SOURCES = "# This file is generated from the following sources:";
      private final HostsSourceDao hostsSourceDao;
      private final HostEntryDao hostEntryDao;
  
      /**
       * Constructor.
       *
       * @param context The application context.
       */
      public RootModel(Context context) {
          super(context);
          // Get DOA
          AppDatabase database = AppDatabase.getInstance(this.context);
          this.hostsSourceDao = database.hostsSourceDao();
          this.hostEntryDao = database.hostEntryDao();
          // Check if host list is applied
          Executor executor = AppExecutors.getInstance().diskIO();
          executor.execute(this::checkApplied);
          executor.execute(() -> syncPreferences(context));
      }
  
      @Override
      public AdBlockMethod getMethod() {
          return ROOT;
      }
  
      @Override
      public void apply() throws HostErrorException {
          setState(R.string.status_apply_sources);
          setState(R.string.status_create_new_hosts);
          createNewHostsFile();
          setState(R.string.status_copy_new_hosts);
          copyNewHostsFile();
          setState(R.string.status_check_copy);
          setState(R.string.status_hosts_updated);
          this.applied.postValue(true);
      }
  
      /**
       * Revert to the default hosts file.
       *
       * @throws HostErrorException If the hosts file could not be reverted.
       */
      @Override
      public void revert() throws HostErrorException {
          // Update status
          setState(R.string.status_revert);
          try {
              // Revert hosts file
              revertHostFile();
              setState(R.string.status_revert_done);
              this.applied.postValue(false);
          } catch (IOException exception) {
              throw new HostErrorException(REVERT_FAIL, exception);
          }
      }
  
      @Override
      public boolean isRecordingLogs() {
          return TcpdumpUtils.isTcpdumpRunning();
      }
  
      @Override
      public void setRecordingLogs(boolean recording) {
          if (recording) {
              TcpdumpUtils.startTcpdump(this.context);
          } else {
              TcpdumpUtils.stopTcpdump();
          }
      }
  
      @Override
      public List<String> getLogs() {
          return TcpdumpUtils.getLogs(this.context);
      }
  
      @Override
      public void clearLogs() {
          TcpdumpUtils.clearLogFile(this.context);
      }
  
      private void checkApplied() {
          boolean applied = false;
  
          Shell.Result result = Shell.cmd("head -n 1 " + ANDROID_SYSTEM_ETC_HOSTS).exec();
          if (!result.isSuccess()) {
              Timber.e("Failed to read first line of hosts file. Error code: %s", result.getCode());
          } else {
              applied = mergeAllLines(result.getOut()).startsWith(HEADER1);
          }
  
          this.applied.postValue(applied);
      }
  
      private void syncPreferences(Context context) {
          if (PreferenceHelper.getWebServerEnabled(context) && !WebServerUtils.isWebServerRunning()) {
              WebServerUtils.startWebServer(context);
          }
      }
  
      private void deleteNewHostsFile() {
          // delete generated hosts file from private storage
          this.context.deleteFile(HOSTS_FILENAME);
      }
  
      private void copyNewHostsFile() throws HostErrorException {
          try {
              copyHostsFile(HOSTS_FILENAME);
          } catch (CommandException exception) {
              throw new HostErrorException(COPY_FAIL, exception);
          }
      }
  
      /**
       * Create a new hosts files in a private file from downloaded hosts sources.
       *
       * @throws HostErrorException If the new hosts file could not be created.
       */
      private void createNewHostsFile() throws HostErrorException {
          deleteNewHostsFile();
          try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(this.context.openFileOutput(HOSTS_FILENAME, MODE_PRIVATE)))) {
              writeHostsHeader(writer);
              writeLoopbackToHosts(writer);
              writeHosts(writer);
          } catch (IOException exception) {
              throw new HostErrorException(PRIVATE_FILE_FAILED, exception);
          }
      }
  
      private void writeHostsHeader(BufferedWriter writer) throws IOException {
          // Format current date
          SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
          Date now = new Date();
          String date = formatter.format(now);
          // Write header
          writer.write(HEADER1);
          writer.write(date);
          writer.newLine();
          writer.write(HEADER2);
          writer.newLine();
          // Write hosts source
          writer.write(HEADER_SOURCES);
          writer.newLine();
          for (HostsSource hostsSource : this.hostsSourceDao.getEnabled()) {
              writer.write("# - " + hostsSource.getLabel() + ":" + hostsSource.getUrl());
              writer.newLine();
          }
          // Write empty line separator
          writer.newLine();
      }
  
      private void writeLoopbackToHosts(BufferedWriter writer) throws IOException {
          writer.write(LOCALHOST_IPV4 + " " + LOCALHOST_HOSTNAME);
          writer.newLine();
          writer.write(LOCALHOST_IPV6 + " " + LOCALHOST_HOSTNAME);
          writer.newLine();
      }
  
      private void writeHosts(BufferedWriter writer) throws IOException {
          // Get user preferences
          String redirectionIpv4 = PreferenceHelper.getRedirectionIpv4(this.context);
          String redirectionIpv6 = PreferenceHelper.getRedirectionIpv6(this.context);
          boolean enableIpv6 = PreferenceHelper.getEnableIpv6(this.context);
          // Write each hostname
          for (HostEntry entry : this.hostEntryDao.getAll()) {
              String hostname = entry.getHost();
              if (entry.getType() == REDIRECTED) {
                  writer.write(entry.getRedirection() + " " + hostname);
                  writer.newLine();
              } else {
                  writer.write(redirectionIpv4 + " " + hostname);
                  writer.newLine();
                  if (enableIpv6) {
                      writer.write(redirectionIpv6 + " " + hostname);
                      writer.newLine();
                  }
              }
          }
      }
  
      /**
       * Revert to default hosts file.
       *
       * @throws IOException If the hosts file could not be reverted.
       */
      private void revertHostFile() throws IOException {
          // Create private file
          try (FileOutputStream fos = this.context.openFileOutput(DEFAULT_HOSTS_FILENAME, MODE_PRIVATE)) {
              // Write default localhost as hosts file
              String localhost = LOCALHOST_IPV4 + " " + LOCALHOST_HOSTNAME + LINE_SEPARATOR +
                      LOCALHOST_IPV6 + " " + LOCALHOST_HOSTNAME + LINE_SEPARATOR;
              fos.write(localhost.getBytes());
              // Copy generated hosts file to target location
              copyHostsFile(DEFAULT_HOSTS_FILENAME);
              // Delete generated hosts file after applying it
              this.context.deleteFile(DEFAULT_HOSTS_FILENAME);
          } catch (Exception exception) {
              throw new IOException("Unable to revert hosts file.", exception);
          }
      }
  
      /**
       * Copy source file from private storage of AdAway to hosts file target using root commands.
       */
      private void copyHostsFile(String source) throws HostErrorException, CommandException {
          String privateDir = this.context.getFilesDir().getAbsolutePath();
          String privateFile = privateDir + File.separator + source;
  
          // if the target has a trailing slash, it is not a valid target!
          String target = ANDROID_SYSTEM_ETC_HOSTS;
          File targetFile = new File(target);
  
          /* check for space on partition */
          long size = new File(privateFile).length();
          Timber.i("Size of hosts file: %s.", size);
          if (!hasEnoughSpaceOnPartition(targetFile, size)) {
              throw new HostErrorException(NOT_ENOUGH_SPACE);
          }
  
          /* Execute commands */
          boolean writable = isWritable(targetFile);
          try {
              if (!writable) {
                  // remount for write access
                  Timber.i("Remounting for RW…");
                  if (!ShellUtils.remountPartition(targetFile, READ_WRITE)) {
                      throw new CommandException("Failed to remount hosts file partition as read-write.");
                  }
              }
              // Copy hosts file then set owner and permissions
              Shell.Result result = Shell.cmd(
                      "dd if=" + privateFile + " of=" + target,
                      COMMAND_CHOWN + " " + target,
                      COMMAND_CHMOD_644 + " " + target
              ).exec();
              if (!result.isSuccess()) {
                  throw new CommandException("Failed to copy hosts file: " + mergeAllLines(result.getErr()));
              }
          } finally {
              if (!writable) {
                  // after all remount target back as read only
                  ShellUtils.remountPartition(targetFile, READ_ONLY);
              }
          }
      }
  
      /**
       * Check if there is enough space on partition where target is located
       *
       * @param size   size of file to put on partition
       * @param target path where to put the file
       * @return true if it will fit on partition of target, false if it will not fit.
       */
      private boolean hasEnoughSpaceOnPartition(File target, long size) {
          long freeSpace = target.getFreeSpace();
          return (freeSpace == 0 || freeSpace > size);
      }
  }
  