public static void rename(
      @NonNull final HybridFile oldFile,
      @NonNull final HybridFile newFile,
      final boolean rootMode,
      @NonNull final Context context,
      @NonNull final ErrorCallBack errorCallBack) {

    new AsyncTask<Void, Void, Void>() {

      private final DataUtils dataUtils = DataUtils.getInstance();

      /**
       * Determines whether double rename is required based on original and new file name regardless
       * of the case-sensitivity of the filesystem
       */
      private final boolean isCaseSensitiveRename =
          oldFile.getSimpleName().equalsIgnoreCase(newFile.getSimpleName())
              && !oldFile.getSimpleName().equals(newFile.getSimpleName());

      /**
       * random string that is appended to file to prevent name collision, max file name is 255
       * bytes
       */
      private static final String TEMP_FILE_EXT = "u0CtHRqWUnvxIaeBQ@nY2umVm9MDyR1P";

      private boolean localRename(@NonNull HybridFile oldFile, @NonNull HybridFile newFile) {
        File file = new File(oldFile.getPath());
        File file1 = new File(newFile.getPath());
        boolean result = false;

        switch (oldFile.getMode()) {
          case FILE:
            int mode = checkFolder(file.getParentFile(), context);
            if (mode == 1 || mode == 0) {
              try {
                RenameOperation.renameFolder(file, file1, context);
              } catch (ShellNotRunningException e) {
                LOG.warn("failed to rename file in local filesystem", e);
              }
              result = !file.exists() && file1.exists();
              if (!result && rootMode) {
                try {
                  RenameFileCommand.INSTANCE.renameFile(file.getPath(), file1.getPath());
                } catch (ShellNotRunningException e) {
                  LOG.warn("failed to rename file in local filesystem", e);
                }
                oldFile.setMode(OpenMode.ROOT);
                newFile.setMode(OpenMode.ROOT);
                result = !file.exists() && file1.exists();
              }
            }
            break;
          case ROOT:
            try {
              result = RenameFileCommand.INSTANCE.renameFile(file.getPath(), file1.getPath());
            } catch (ShellNotRunningException e) {
              LOG.warn("failed to rename file in root", e);
            }
            newFile.setMode(OpenMode.ROOT);
            break;
        }
        return result;
      }

      private boolean localDoubleRename(@NonNull HybridFile oldFile, @NonNull HybridFile newFile) {
        HybridFile tempFile = new HybridFile(oldFile.mode, oldFile.getPath().concat(TEMP_FILE_EXT));
        if (localRename(oldFile, tempFile)) {
          if (localRename(tempFile, newFile)) {
            return true;
          } else {
            // attempts to rollback
            // changes the temporary file name back to original file name
            LOG.warn("reverting temporary file rename");
            return localRename(tempFile, oldFile);
          }
        }
        return false;
      }

      private Function<DocumentFile, Void> safRenameFile =
          input -> {
            boolean result = false;
            try {
              result = input.renameTo(newFile.getName(context));
            } catch (Exception e) {
              LOG.warn(getClass().getSimpleName(), "Failed to rename", e);
            }
            errorCallBack.done(newFile, result);
            return null;
          };

      @Override
      protected Void doInBackground(Void... params) {
        // check whether file names for new file are valid or recursion occurs.
        // If rename is on OTG, we are skipping
        if (!Operations.isFileNameValid(newFile.getName(context))) {
          errorCallBack.invalidName(newFile);
          return null;
        }

        if (newFile.exists() && !isCaseSensitiveRename) {
          errorCallBack.exists(newFile);
          return null;
        }

        if (oldFile.isSmb()) {
          try {
            SmbFile smbFile = oldFile.getSmbFile();
            // FIXME: smbFile1 should be created from SmbUtil too so it can be mocked
            SmbFile smbFile1 = new SmbFile(new URL(newFile.getPath()), smbFile.getContext());
            if (newFile.exists()) {
              errorCallBack.exists(newFile);
              return null;
            }
            smbFile.renameTo(smbFile1);
            if (!smbFile.exists() && smbFile1.exists()) errorCallBack.done(newFile, true);
          } catch (SmbException | MalformedURLException e) {
            String errmsg =
                context.getString(
                    R.string.cannot_rename_file,
                    HybridFile.parseAndFormatUriForDisplay(oldFile.getPath()),
                    e.getMessage());
            try {
              ArrayList<HybridFileParcelable> failedOps = new ArrayList<>();
              failedOps.add(new HybridFileParcelable(oldFile.getSmbFile()));
              context.sendBroadcast(
                  new Intent(TAG_INTENT_FILTER_GENERAL)
                      .putParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS, failedOps));
            } catch (SmbException exceptionThrownDuringBuildParcelable) {
              LOG.error(
                  "Error creating HybridFileParcelable", exceptionThrownDuringBuildParcelable);
            }
            LOG.error(errmsg, e);
          }
          return null;
        } else if (oldFile.isSftp()) {
          SshClientUtils.execute(
              new SFtpClientTemplate<Void>(oldFile.getPath(), true) {
                @Override
                public Void execute(@NonNull SFTPClient client) {
                  try {
                    client.rename(
                        NetCopyClientUtils.extractRemotePathFrom(oldFile.getPath()),
                        NetCopyClientUtils.extractRemotePathFrom(newFile.getPath()));
                    errorCallBack.done(newFile, true);
                  } catch (IOException e) {
                    String errmsg =
                        context.getString(
                            R.string.cannot_rename_file,
                            HybridFile.parseAndFormatUriForDisplay(oldFile.getPath()),
                            e.getMessage());
                    LOG.error(errmsg);
                    ArrayList<HybridFileParcelable> failedOps = new ArrayList<>();
                    // Nobody care the size or actual permission here. Put a simple "r" and zero
                    // here
                    failedOps.add(
                        new HybridFileParcelable(
                            oldFile.getPath(),
                            "r",
                            oldFile.lastModified(),
                            0,
                            oldFile.isDirectory(context)));
                    context.sendBroadcast(
                        new Intent(TAG_INTENT_FILTER_GENERAL)
                            .putParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS, failedOps));
                    errorCallBack.done(newFile, false);
                  }
                  return null;
                }
              });
        } else if (oldFile.isFtp()) {
          NetCopyClientUtils.INSTANCE.execute(
              new FtpClientTemplate<Boolean>(oldFile.getPath(), false) {
                public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient)
                    throws IOException {
                  boolean result =
                      ftpClient.rename(
                          NetCopyClientUtils.extractRemotePathFrom(oldFile.getPath()),
                          NetCopyClientUtils.extractRemotePathFrom(newFile.getPath()));
                  errorCallBack.done(newFile, result);
                  return result;
                }
              });
        } else if (oldFile.isDropBoxFile()) {
          CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX);
          try {
            cloudStorageDropbox.move(
                CloudUtil.stripPath(OpenMode.DROPBOX, oldFile.getPath()),
                CloudUtil.stripPath(OpenMode.DROPBOX, newFile.getPath()));
            errorCallBack.done(newFile, true);
          } catch (Exception e) {
            LOG.warn("failed to rename file in cloud connection", e);
            errorCallBack.done(newFile, false);
          }
        } else if (oldFile.isBoxFile()) {
          CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX);
          try {
            cloudStorageBox.move(
                CloudUtil.stripPath(OpenMode.BOX, oldFile.getPath()),
                CloudUtil.stripPath(OpenMode.BOX, newFile.getPath()));
            errorCallBack.done(newFile, true);
          } catch (Exception e) {
            LOG.warn("failed to rename file in cloud connection", e);
            errorCallBack.done(newFile, false);
          }
        } else if (oldFile.isOneDriveFile()) {
          CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE);
          try {
            cloudStorageOneDrive.move(
                CloudUtil.stripPath(OpenMode.ONEDRIVE, oldFile.getPath()),
                CloudUtil.stripPath(OpenMode.ONEDRIVE, newFile.getPath()));
            errorCallBack.done(newFile, true);
          } catch (Exception e) {
            LOG.warn("failed to rename file in cloud connection", e);
            errorCallBack.done(newFile, false);
          }
        } else if (oldFile.isGoogleDriveFile()) {
          CloudStorage cloudStorageGdrive = dataUtils.getAccount(OpenMode.GDRIVE);
          try {
            cloudStorageGdrive.move(
                CloudUtil.stripPath(OpenMode.GDRIVE, oldFile.getPath()),
                CloudUtil.stripPath(OpenMode.GDRIVE, newFile.getPath()));
            errorCallBack.done(newFile, true);
          } catch (Exception e) {
            LOG.warn("failed to rename file in cloud connection", e);
            errorCallBack.done(newFile, false);
          }
        } else if (oldFile.isOtgFile()) {
          if (checkOtgNewFileExists(newFile, context)) {
            errorCallBack.exists(newFile);
            return null;
          }
          safRenameFile.apply(OTGUtil.getDocumentFile(oldFile.getPath(), context, false));
          return null;
        } else if (oldFile.isDocumentFile()) {
          if (checkDocumentFileNewFileExists(newFile, context)) {
            errorCallBack.exists(newFile);
            return null;
          }
          safRenameFile.apply(
              OTGUtil.getDocumentFile(
                  oldFile.getPath(),
                  SafRootHolder.getUriRoot(),
                  context,
                  OpenMode.DOCUMENT_FILE,
                  false));
          return null;
        } else {
          File file = new File(oldFile.getPath());
          if (oldFile.getMode() == OpenMode.FILE) {
            int mode = checkFolder(file.getParentFile(), context);
            if (mode == 2) {
              errorCallBack.launchSAF(oldFile, newFile);
            }
          }

          boolean result;
          if (isCaseSensitiveRename) {
            result = localDoubleRename(oldFile, newFile);
          } else {
            result = localRename(oldFile, newFile);
          }
          errorCallBack.done(newFile, result);
        }
        return null;
      }

      @Override
      protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        if (newFile != null && oldFile != null) {
          HybridFile[] hybridFiles = {newFile, oldFile};
          MediaConnectionUtils.scanFile(context, hybridFiles);
        }
      }
    }.executeOnExecutor(executor);
  }
@Test
  public void testRename() throws InterruptedException {
    File oldFolder = new File(storageRoot, "test1");
    HybridFile oldFolderHF = new HybridFile(OpenMode.FILE, oldFolder.getAbsolutePath());
    File newFolder = new File(storageRoot, "test2");
    HybridFile newFolderHF = new HybridFile(OpenMode.FILE, newFolder.getAbsolutePath());

    CountDownLatch waiter1 = new CountDownLatch(1);
    Operations.mkdir(
        newFolderHF,
        oldFolderHF,
        ApplicationProvider.getApplicationContext(),
        false,
        new AbstractErrorCallback() {
          @Override
          public void done(HybridFile hFile, boolean b) {
            waiter1.countDown();
          }
        });
    waiter1.await();
    assertTrue(oldFolder.exists());

    CountDownLatch waiter2 = new CountDownLatch(1);
    Operations.rename(
        oldFolderHF,
        newFolderHF,
        false,
        ApplicationProvider.getApplicationContext(),
        new AbstractErrorCallback() {
          @Override
          public void done(HybridFile hFile, boolean b) {
            waiter2.countDown();
          }
        });
    waiter2.await();
    assertFalse(oldFolder.exists());
    assertTrue(newFolder.exists());
  }