Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve file handling in a few places #1719

Merged
merged 6 commits into from
Jun 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ public boolean checkPermissions() {
public boolean saveDocument(final boolean forceSaveEmpty) {
// Document is written iff writeable && content has changed
if (checkPermissions() && _hlEditor != null && isAdded()) {
if (_document.saveContent(getContext(), _hlEditor.getText().toString(), _shareUtil, forceSaveEmpty)) {
if (_document.saveContent(getContext(), _hlEditor.getText(), _shareUtil, forceSaveEmpty)) {
checkTextChangeState();
return true;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
import net.gsantner.opoc.util.PermissionChecker;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

public class OpenEditorActivity extends MarkorBaseActivity {
protected void openEditorForFile(File file) {
Expand All @@ -42,9 +40,7 @@ protected void openActivityAndClose(final Intent openIntent, File file) {
file.getParentFile().mkdirs();
}
if (!file.exists() && !file.isDirectory()) {
final Map<String, Object> options = new HashMap<>(1);
options.put(FileUtils.WITH_BOM, new AppSettings(getApplicationContext()).getNewFileDialogLastUsedUtf8Bom());
FileUtils.writeFile(file, "", options);
FileUtils.writeFile(file, "", new FileUtils.FileInfo().withBom(new AppSettings(getApplicationContext()).getNewFileDialogLastUsedUtf8Bom()));
}
openIntent.putExtra(Document.EXTRA_PATH, openIntent.hasExtra(Document.EXTRA_PATH) ? openIntent.getSerializableExtra(Document.EXTRA_PATH) : file);
new ActivityUtils(this).animateToActivity(openIntent, true, 1).freeContextRef();
Expand Down
45 changes: 26 additions & 19 deletions app/src/main/java/net/gsantner/markor/model/Document.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import net.gsantner.markor.util.AppSettings;
import net.gsantner.markor.util.ShareUtil;
import net.gsantner.opoc.util.FileUtils;
import net.gsantner.opoc.util.StringUtils;

import java.io.File;
import java.io.FileInputStream;
Expand All @@ -36,9 +37,7 @@
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import other.de.stanetz.jpencconverter.JavaPasswordbasedCryption;

Expand All @@ -55,7 +54,7 @@ public class Document implements Serializable {
private String _title = "";
private String _path = "";
private long _modTime = 0;
private boolean _withBom = false;
private FileUtils.FileInfo _fileInfo;
@StringRes
private int _format = TextFormat.FORMAT_UNKNOWN;

Expand Down Expand Up @@ -156,11 +155,11 @@ public boolean isEncrypted() {

private void setContentHash(final CharSequence s) {
_lastLength = s.length();
_lastHash = FileUtils.crc32(s.toString().getBytes());
gsantner marked this conversation as resolved.
Show resolved Hide resolved
_lastHash = FileUtils.crc32(s);
}

public boolean isContentSame(final CharSequence s) {
return s != null && s.length() == _lastLength && _lastHash == (FileUtils.crc32(s.toString().getBytes()));
return s != null && s.length() == _lastLength && _lastHash == FileUtils.crc32(s);
}

public synchronized String loadContent(final Context context) {
Expand All @@ -183,9 +182,9 @@ public synchronized String loadContent(final Context context) {
content = "";
}
} else {
final Pair<String, Map<String, Object>> result = FileUtils.readTextFileFast(_file);
final Pair<String, FileUtils.FileInfo> result = FileUtils.readTextFileFast(_file);
content = result.first;
_withBom = (boolean) result.second.get(FileUtils.WITH_BOM);
_fileInfo = result.second;
}

if (MainActivity.IS_DEBUG_ENABLED) {
Expand Down Expand Up @@ -230,22 +229,26 @@ public static boolean testCreateParent(final File file) {
}
}

public boolean saveContent(final Context context, final String content) {
public boolean saveContent(final Context context, final CharSequence content) {
return saveContent(context, content, null, false);
}

public synchronized boolean saveContent(final Context context, final String content, ShareUtil shareUtil, boolean isManualSave) {
if (!isManualSave && content.trim().length() < ShareUtil.MIN_OVERWRITE_LENGTH) {
// Doing as we don't want to convert to string or copy unless necessary
private static int trimmedLength(final CharSequence c) {
return StringUtils.getLastNonWhitespace(c, c.length()) - StringUtils.getNextNonWhitespace(c, 0);
}

public synchronized boolean saveContent(final Context context, final CharSequence content, ShareUtil shareUtil, boolean isManualSave) {
if (!isManualSave && trimmedLength(content) < ShareUtil.MIN_OVERWRITE_LENGTH) {
return false;
}

if (!testCreateParent()) {
return false;
}
shareUtil = shareUtil != null ? shareUtil : new ShareUtil(context);

// Don't write same content if base file not changed
if (isContentSame(content) && _modTime >= lastModified()) {
if (_modTime >= lastModified() && isContentSame(content)) {
return true;
}

Expand All @@ -254,15 +257,17 @@ public synchronized boolean saveContent(final Context context, final String cont
final char[] pw;
final byte[] contentAsBytes;
if (isEncrypted() && (pw = getPasswordWithWarning(context)) != null) {
contentAsBytes = new JavaPasswordbasedCryption(Build.VERSION.SDK_INT, new SecureRandom()).encrypt(content, pw);
contentAsBytes = new JavaPasswordbasedCryption(Build.VERSION.SDK_INT, new SecureRandom()).encrypt(content.toString(), pw);
} else {
contentAsBytes = content.getBytes();
contentAsBytes = content.toString().getBytes();
}

shareUtil = shareUtil != null ? shareUtil : new ShareUtil(context);

if (shareUtil.isUnderStorageAccessFolder(_file)) {
shareUtil.writeFile(_file, false, (fileOpened, fos) -> {
try {
if (_withBom) {
if (_fileInfo != null && _fileInfo.hasBom) {
fos.write(0xEF);
fos.write(0xBB);
fos.write(0xBF);
Expand All @@ -274,9 +279,7 @@ public synchronized boolean saveContent(final Context context, final String cont
});
success = true;
} else {
final Map<String, Object> options = new HashMap<>(1);
options.put(FileUtils.WITH_BOM, _withBom);
success = FileUtils.writeFile(_file, contentAsBytes, options);
success = FileUtils.writeFile(_file, contentAsBytes, _fileInfo);
}
} catch (JavaPasswordbasedCryption.EncryptionFailedException e) {
Log.e(Document.class.getName(), "writeContent: encrypt failed for File " + getPath() + ". " + e.getMessage(), e);
Expand All @@ -286,7 +289,11 @@ public synchronized boolean saveContent(final Context context, final String cont

if (success) {
setContentHash(content);
_modTime = lastModified();
final long curModTime = lastModified();
if (_modTime >= curModTime) {
Log.w("MARKOR_DOCUMENT", "File modification time unchanged after write");
}
_modTime = curModTime;
}

return success;
Expand Down
55 changes: 36 additions & 19 deletions app/src/main/java/net/gsantner/opoc/util/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#########################################################*/
package net.gsantner.opoc.util;


import android.text.TextUtils;
import android.util.Pair;

Expand All @@ -26,44 +25,53 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.zip.CRC32;

@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "deprecation", "TryFinallyCanBeTryWithResources"})
@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "TryFinallyCanBeTryWithResources"})
public class FileUtils {
// Used on methods like copyFile(src, dst)
private static final int BUFFER_SIZE = 4096;

public final static String WITH_BOM = "withBom";
/**
* Info of various types about a file
*/
public static class FileInfo implements Serializable {
public boolean hasBom = false;

public FileInfo withBom(boolean bom) {
hasBom = bom;
return this;
}
}

public static Pair<String, Map<String, Object>> readTextFileFast(final File file) {
Map<String, Object> info = new HashMap<>(1);
public static Pair<String, FileInfo> readTextFileFast(final File file) {
final FileInfo info = new FileInfo();

try (final FileInputStream inputStream = new FileInputStream(file)) {
final ByteArrayOutputStream result = new ByteArrayOutputStream();

final byte[] bomBuffer = new byte[3];
int bomReadLength = inputStream.read(bomBuffer);
boolean withBom = bomReadLength == 3 &&
final int bomReadLength = inputStream.read(bomBuffer);
info.withBom(bomReadLength == 3 &&
bomBuffer[0] == (byte) 0xEF &&
bomBuffer[1] == (byte) 0xBB &&
bomBuffer[2] == (byte) 0xBF;
info.put(WITH_BOM, withBom);
bomBuffer[2] == (byte) 0xBF
);

if (!withBom && bomReadLength > 0) {
if (!info.hasBom && bomReadLength > 0) {
result.write(bomBuffer, 0, bomReadLength);
}
if (bomReadLength < 3) {
Expand Down Expand Up @@ -201,11 +209,9 @@ public static byte[] readCloseBinaryStream(final InputStream stream) {
return baos.toByteArray();
}

public static boolean writeFile(final File file, final byte[] data, final Map<String, Object> options) {
boolean withBom = options != null && (Boolean) options.get(WITH_BOM);

public static boolean writeFile(final File file, final byte[] data, final FileInfo options) {
try (final FileOutputStream output = new FileOutputStream(file)) {
if (withBom) {
if (options != null && options.hasBom) {
output.write(0xEF);
output.write(0xBB);
output.write(0xBF);
Expand All @@ -219,7 +225,7 @@ public static boolean writeFile(final File file, final byte[] data, final Map<St
}
}

public static boolean writeFile(final File file, final String data, final Map<String, Object> options) {
public static boolean writeFile(final File file, final String data, final FileInfo options) {
return writeFile(file, data.getBytes(), options);
}

Expand Down Expand Up @@ -308,7 +314,7 @@ public static boolean deleteRecursive(final File file) {
boolean ok = true;
if (file.exists()) {
if (file.isDirectory()) {
for (File child : file.listFiles())
for (final File child : file.listFiles())
ok &= deleteRecursive(child);
}
ok &= file.delete();
Expand Down Expand Up @@ -352,7 +358,6 @@ public static boolean renameFile(File srcFile, File destFile) {
return true;
}

@SuppressWarnings("ResultOfMethodCallIgnored")
public static boolean renameFileInSameFolder(File srcFile, String destFilename) {
return renameFile(srcFile, new File(srcFile.getParent(), destFilename));
}
Expand Down Expand Up @@ -553,6 +558,18 @@ public static String sha512(final byte[] data) {
return hash(data, "SHA-512");
}

public static long crc32(final CharSequence data) {
final CRC32 alg = new CRC32();
final int length = data.length();
for (int i = 0; i < length; i++) {
final char c = data.charAt(i);
// Upper and lower bytes
alg.update((byte) (c & 0xff));
alg.update((byte) (c >> 8));
}
return alg.getValue();
}

public static long crc32(final byte[] data) {
final CRC32 alg = new CRC32();
alg.update(data);
Expand Down
26 changes: 15 additions & 11 deletions app/src/main/java/net/gsantner/opoc/util/StringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,25 @@ public static int getLineEnd(CharSequence s, int start, int maxRange) {
return i;
}

public static int getNextNonWhitespace(CharSequence s, int start) {
return getNextNonWhitespace(s, start, s.length());
public static int getLastNonWhitespace(final CharSequence s, final int start) {
for (int i = Math.min(s.length() - 1, start); i >= 0; i--) {
char c = s.charAt(i);
if (c != ' ' && c != '\t') {
return i;
}
}
return -1;
}

public static int getNextNonWhitespace(CharSequence s, int start, int maxRange) {
int i = start;
if (isValidIndex(s, start, maxRange - 1)) {
for (; i < maxRange; i++) {
char c = s.charAt(i);
if (c != ' ' && c != '\t') {
break;
}
public static int getNextNonWhitespace(final CharSequence s, final int start) {
final int length = s.length();
for (int i = Math.max(0, start); i < length; i++) {
char c = s.charAt(i);
if (c != ' ' && c != '\t') {
return i;
}
}
return i;
return -1;
}

public static boolean isNullOrWhitespace(String str) {
Expand Down