Skip to content
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
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ android {
applicationId "com.codedead.deadhash"
minSdk 30
targetSdk 37
versionName '1.8.4'
versionName '1.8.5'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 14
versionCode 16
}
buildTypes {
release {
Expand Down
201 changes: 198 additions & 3 deletions app/src/main/java/com/codedead/deadhash/gui/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.codedead.deadhash.domain.objects.hashgenerator.TextHashGenerator;
import com.codedead.deadhash.domain.objects.settings.SettingsContainer;
import com.codedead.deadhash.domain.utils.IntentUtils;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;

import androidx.core.view.GravityCompat;
Expand Down Expand Up @@ -56,6 +57,11 @@
import com.codedead.deadhash.domain.objects.hashgenerator.HashData;
import com.codedead.deadhash.domain.utils.LocaleHelper;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
Expand Down Expand Up @@ -93,6 +99,14 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
private Uri fileUri;
private ActivityResultLauncher<Intent> activityResultLauncher;

private ActivityResultLauncher<String> fileTxtExportLauncher;
private ActivityResultLauncher<String> fileCsvExportLauncher;
private ActivityResultLauncher<String> textTxtExportLauncher;
private ActivityResultLauncher<String> textCsvExportLauncher;

private FloatingActionButton fabExportFile;
private FloatingActionButton fabExportText;

@Override
protected void onCreate(final Bundle savedInstanceState) {
settingsContainer.loadSettings(getApplicationContext());
Expand Down Expand Up @@ -145,6 +159,7 @@ protected void onCreate(final Bundle savedInstanceState) {
loadAboutContent();

loadAlertContent();
registerExportLaunchers();

this.activityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
Expand All @@ -160,6 +175,7 @@ protected void onCreate(final Bundle savedInstanceState) {

fileDataArrayList.clear();
mAdapterFile.notifyDataSetChanged();
updateExportVisibility();
}
}
} else {
Expand Down Expand Up @@ -206,10 +222,9 @@ private void handleSendFile(final Intent intent) {

fileDataArrayList.clear();
mAdapterFile.notifyDataSetChanged();
updateExportVisibility();
}
}
} else {
Toast.makeText(getApplicationContext(), R.string.error_open_file, Toast.LENGTH_SHORT).show();
}
}

Expand Down Expand Up @@ -336,6 +351,9 @@ private void loadFileHashContent(final Bundle savedInstance) {
final Button btnGenerate = findViewById(R.id.ButtonGenerateFile);
edtFileCompare = findViewById(R.id.Edit_FileCompare);

fabExportFile = findViewById(R.id.FabExportFile);
fabExportFile.setOnClickListener(v -> showExportDialog(true));

if (savedInstance != null) {
if (savedInstance.containsKey("FILE_PATH")) {
edtFilePath.setText(savedInstance.getString("FILE_PATH"));
Expand All @@ -353,6 +371,7 @@ private void loadFileHashContent(final Bundle savedInstance) {
}

mRecyclerViewFile.setAdapter(mAdapterFile);
updateExportVisibility();

btnOpenFile.setOnClickListener(this::onClickSelectFile);
edtFilePath.setOnClickListener(this::onClickSelectFile);
Expand All @@ -374,6 +393,7 @@ private void loadFileHashContent(final Bundle savedInstance) {

fileDataArrayList.clear();
mAdapterFile.notifyDataSetChanged();
updateExportVisibility();

String compare = "";
if (edtFileCompare.getText() != null) {
Expand All @@ -392,6 +412,7 @@ private void loadFileHashContent(final Bundle savedInstance) {
fileDataArrayList.add(d);
mAdapterFile.notifyItemInserted(fileDataArrayList.size());
}
updateExportVisibility();
}));

pgbFile.setVisibility(View.VISIBLE);
Expand All @@ -414,6 +435,9 @@ private void loadTextHashContent(final Bundle savedInstance) {
final Button btnGenerate = findViewById(R.id.ButtonGenerateText);
edtTextCompare = findViewById(R.id.Edit_TextCompare);

fabExportText = findViewById(R.id.FabExportText);
fabExportText.setOnClickListener(v -> showExportDialog(false));

if (savedInstance != null) {
if (savedInstance.containsKey("TEXT_DATA")) {
edtTextData.setText(savedInstance.getString("TEXT_DATA"));
Expand All @@ -431,6 +455,7 @@ private void loadTextHashContent(final Bundle savedInstance) {
}

mRecyclerViewText.setAdapter(mAdapterText);
updateExportVisibility();

btnGenerate.setOnClickListener(v -> {
if (textLoading) return;
Expand All @@ -441,8 +466,9 @@ private void loadTextHashContent(final Bundle savedInstance) {

mRecyclerViewText.setAdapter(mAdapterText);

fileDataArrayList.clear();
textDataArrayList.clear();
mAdapterText.notifyDataSetChanged();
updateExportVisibility();

if (edtTextData.getText() == null || edtTextData.getText().toString().isEmpty()) {
Toast.makeText(MainActivity.this, R.string.toast_error_notext, Toast.LENGTH_SHORT).show();
Expand All @@ -468,12 +494,181 @@ private void loadTextHashContent(final Bundle savedInstance) {
textDataArrayList.add(d);
mAdapterText.notifyItemInserted(textDataArrayList.size());
}
updateExportVisibility();
}));

pgbText.setVisibility(View.VISIBLE);
});
}

/**
* Register the ActivityResultLaunchers that are used to export hashes
*/
private void registerExportLaunchers() {
fileTxtExportLauncher = registerForActivityResult(
new ActivityResultContracts.CreateDocument("text/plain"),
uri -> exportHashes(uri, fileDataArrayList, false));

fileCsvExportLauncher = registerForActivityResult(
new ActivityResultContracts.CreateDocument("text/csv"),
uri -> exportHashes(uri, fileDataArrayList, true));

textTxtExportLauncher = registerForActivityResult(
new ActivityResultContracts.CreateDocument("text/plain"),
uri -> exportHashes(uri, textDataArrayList, false));

textCsvExportLauncher = registerForActivityResult(
new ActivityResultContracts.CreateDocument("text/csv"),
uri -> exportHashes(uri, textDataArrayList, true));
}

/**
* Show a dialog that lets the user choose the format to export the hashes in
*
* @param fileTab True if the file hashes should be exported, false for the text hashes
*/
private void showExportDialog(final boolean fileTab) {
final List<HashData> hashDataList = fileTab ? fileDataArrayList : textDataArrayList;
if (hashDataList.isEmpty()) return;

final String[] formats = {
getString(R.string.export_txt),
getString(R.string.export_csv)
};

new AlertDialog.Builder(this)
.setTitle(R.string.export_dialog_title)
.setItems(formats, (dialog, which) -> {
if (which == 0) {
(fileTab ? fileTxtExportLauncher : textTxtExportLauncher)
.launch(getExportFileName(fileTab, "txt"));
} else {
(fileTab ? fileCsvExportLauncher : textCsvExportLauncher)
.launch(getExportFileName(fileTab, "csv"));
}
})
.show();
}

/**
* Show or hide the export buttons based on whether hashes are available
*/
private void updateExportVisibility() {
if (fabExportFile != null) {
fabExportFile.setVisibility(fileDataArrayList.isEmpty() ? View.GONE : View.VISIBLE);
}
if (fabExportText != null) {
fabExportText.setVisibility(textDataArrayList.isEmpty() ? View.GONE : View.VISIBLE);
}
}

/**
* Get a suggested file name for an export
*
* @param fileTab True if the file hashes are exported, false for the text hashes
* @param extension The file extension, without a dot
* @return The suggested file name
*/
private String getExportFileName(final boolean fileTab, final String extension) {
String source = "text";
if (fileTab) {
source = edtFilePath.getText().toString().trim();
if (source.isEmpty()) {
source = "file";
} else {
final int dotIndex = source.lastIndexOf('.');
if (dotIndex > 0) {
source = source.substring(0, dotIndex);
}
}
}

source = source.replaceAll("[\\\\/:*?\"<>|]", "_");
if (source.length() > 60) {
source = source.substring(0, 60);
}

return "DeadHash_" + source + "_hashes." + extension;
}

/**
* Write hashes to the given Uri
*
* @param uri The Uri to write to, or null when the user cancelled the file picker
* @param hashDataList The List of HashData objects that should be exported
* @param csv True to export as CSV, false to export as plain text
*/
private void exportHashes(final Uri uri, final List<HashData> hashDataList, final boolean csv) {
if (uri == null || hashDataList == null || hashDataList.isEmpty()) return;

final String content = csv ? buildCsvContent(hashDataList) : buildTxtContent(hashDataList);

CompletableFuture.runAsync(() -> {
try (final OutputStream outputStream = getContentResolver().openOutputStream(uri, "w")) {
if (outputStream == null) {
throw new IOException("Unable to open the selected file");
}

try (final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
writer.write(content);
}
} catch (final IOException ex) {
runOnUiThread(() -> Toast.makeText(getApplicationContext(), R.string.export_error, Toast.LENGTH_SHORT).show());
return;
}

runOnUiThread(() -> Toast.makeText(getApplicationContext(), R.string.export_success, Toast.LENGTH_SHORT).show());
});
}

/**
* Build the plain text representation of a List of HashData objects
*
* @param hashDataList The List of HashData objects
* @return The plain text representation
*/
private static String buildTxtContent(final List<HashData> hashDataList) {
final StringBuilder builder = new StringBuilder();
for (final HashData hashData : hashDataList) {
builder.append(hashData.getHashName())
.append(": ")
.append(hashData.getHashData())
.append('\n');
}
return builder.toString();
}

/**
* Build the CSV representation of a List of HashData objects
*
* @param hashDataList The List of HashData objects
* @return The CSV representation
*/
private static String buildCsvContent(final List<HashData> hashDataList) {
final StringBuilder builder = new StringBuilder("Algorithm,Hash\r\n");
for (final HashData hashData : hashDataList) {
builder.append(escapeCsv(hashData.getHashName()))
.append(',')
.append(escapeCsv(hashData.getHashData()))
.append("\r\n");
}
return builder.toString();
}

/**
* Escape a value for use in a CSV file
*
* @param value The value to escape
* @return The escaped value
*/
private static String escapeCsv(final String value) {
if (value == null || value.isEmpty()) return "";
if (value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r")) {
return "\"" + value.replace("\"", "\"\"") + "\"";
}
return value;
}

/**
* Get the List of HashAlgorithm enums that can be used, according to user preferences
*
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_export.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
</vector>
24 changes: 21 additions & 3 deletions app/src/main/res/layout/content_file.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,29 @@
android:layout_height="wrap_content"
android:visibility="gone" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/file_recycler"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

</androidx.recyclerview.widget.RecyclerView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/file_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="88dp" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/FabExportFile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/export"
android:src="@drawable/ic_export"
android:visibility="gone"
app:backgroundTint="?attr/colorPrimary"
app:tint="?attr/colorOnPrimary" />

</FrameLayout>

</LinearLayout>
24 changes: 21 additions & 3 deletions app/src/main/res/layout/content_text.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,29 @@
android:layout_height="wrap_content"
android:visibility="gone" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/text_recycler"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

</androidx.recyclerview.widget.RecyclerView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/text_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="88dp" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/FabExportText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/export"
android:src="@drawable/ic_export"
android:visibility="gone"
app:backgroundTint="?attr/colorPrimary"
app:tint="?attr/colorOnPrimary" />

</FrameLayout>

</LinearLayout>
Loading