multi file and multi dumpstate support

This commit is contained in:
2025-04-15 13:15:47 -04:00
parent 2e4d02b503
commit 418520c000
11 changed files with 1112 additions and 27 deletions

View File

@@ -0,0 +1,212 @@
package com.mattintech.simplelogupload.adapter;
import android.content.Context;
import android.text.format.DateFormat;
import android.text.format.Formatter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.mattintech.simplelogupload.R;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Adapter for displaying dumpstate files in a RecyclerView with multi-selection capability
*/
public class DumpstateAdapter extends RecyclerView.Adapter<DumpstateAdapter.ViewHolder> {
private final List<File> files;
private final Context context;
private final Set<Integer> selectedPositions = new HashSet<>();
private OnSelectionChangedListener selectionChangedListener;
/**
* Constructor
*
* @param context The context
* @param files List of dumpstate files
*/
public DumpstateAdapter(Context context, List<File> files) {
this.context = context;
this.files = files;
}
/**
* Set a listener for selection changes
*
* @param listener The listener
*/
public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
this.selectionChangedListener = listener;
}
/**
* Get the list of selected files
*
* @return List of selected files
*/
public List<File> getSelectedFiles() {
List<File> selectedFiles = new ArrayList<>();
for (int position : selectedPositions) {
if (position >= 0 && position < files.size()) {
selectedFiles.add(files.get(position));
}
}
return selectedFiles;
}
/**
* Check if an item is selected
*
* @param position The position to check
* @return true if selected, false otherwise
*/
public boolean isSelected(int position) {
return selectedPositions.contains(position);
}
/**
* Select or deselect an item
*
* @param position The position to select/deselect
* @param selected Whether to select (true) or deselect (false)
*/
public void setSelected(int position, boolean selected) {
if (selected) {
selectedPositions.add(position);
} else {
selectedPositions.remove(position);
}
notifyItemChanged(position);
if (selectionChangedListener != null) {
selectionChangedListener.onSelectionChanged(selectedPositions.size());
}
}
/**
* Toggle selection state of an item
*
* @param position The position to toggle
*/
public void toggleSelection(int position) {
setSelected(position, !isSelected(position));
}
/**
* Select all items
*/
public void selectAll() {
selectedPositions.clear();
for (int i = 0; i < files.size(); i++) {
selectedPositions.add(i);
}
notifyDataSetChanged();
if (selectionChangedListener != null) {
selectionChangedListener.onSelectionChanged(selectedPositions.size());
}
}
/**
* Clear all selections
*/
public void clearSelections() {
selectedPositions.clear();
notifyDataSetChanged();
if (selectionChangedListener != null) {
selectionChangedListener.onSelectionChanged(0);
}
}
/**
* Get the number of selected items
*
* @return The count of selected items
*/
public int getSelectedCount() {
return selectedPositions.size();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_dumpstate_file, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
File file = files.get(position);
// Set file name
holder.fileNameText.setText(file.getName());
// Add tooltip for long filenames
holder.fileNameText.setOnLongClickListener(v -> {
Toast.makeText(context, file.getName(), Toast.LENGTH_LONG).show();
return true;
});
// Format and set file date
Date lastModified = new Date(file.lastModified());
String formattedDate = DateFormat.format("MMM dd, yyyy hh:mm a", lastModified).toString();
holder.fileDateText.setText(context.getString(R.string.dumpstate_date_format, formattedDate));
// Format and set file size
String fileSize = Formatter.formatFileSize(context, file.length());
holder.fileSizeText.setText(context.getString(R.string.dumpstate_size_format, fileSize));
// Set checkbox state
holder.checkBox.setChecked(isSelected(position));
// Set click listeners
holder.checkBox.setOnClickListener(v -> toggleSelection(holder.getAbsoluteAdapterPosition()));
holder.itemView.setOnClickListener(v -> toggleSelection(holder.getAbsoluteAdapterPosition()));
}
@Override
public int getItemCount() {
return files.size();
}
/**
* ViewHolder for dumpstate file items
*/
static class ViewHolder extends RecyclerView.ViewHolder {
final CheckBox checkBox;
final TextView fileNameText;
final TextView fileDateText;
final TextView fileSizeText;
ViewHolder(View itemView) {
super(itemView);
checkBox = itemView.findViewById(R.id.checkBox);
fileNameText = itemView.findViewById(R.id.fileNameText);
fileDateText = itemView.findViewById(R.id.fileDateText);
fileSizeText = itemView.findViewById(R.id.fileSizeText);
}
}
/**
* Interface for handling selection changes
*/
public interface OnSelectionChangedListener {
void onSelectionChanged(int selectedCount);
}
}

View File

@@ -0,0 +1,146 @@
package com.mattintech.simplelogupload.adapter;
import android.content.Context;
import android.text.format.DateFormat;
import android.text.format.Formatter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.mattintech.simplelogupload.R;
import java.io.File;
import java.util.Date;
import java.util.List;
/**
* Adapter for displaying dumpstate files in a RecyclerView
*/
public class DumpstateFileAdapter extends RecyclerView.Adapter<DumpstateFileAdapter.ViewHolder> {
private final List<File> files;
private final Context context;
private int selectedPosition = -1;
private OnItemSelectedListener listener;
/**
* Constructor
*
* @param context The context
* @param files List of dumpstate files
*/
public DumpstateFileAdapter(Context context, List<File> files) {
this.context = context;
this.files = files;
}
/**
* Set a listener for item selection
*
* @param listener The listener
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
this.listener = listener;
}
/**
* Get the currently selected file
*
* @return The selected file or null if none selected
*/
public File getSelectedFile() {
if (selectedPosition >= 0 && selectedPosition < files.size()) {
return files.get(selectedPosition);
}
return null;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_dumpstate_file, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
File file = files.get(position);
// Set file name
holder.fileNameText.setText(file.getName());
// Add tooltip for long filenames
holder.fileNameText.setOnLongClickListener(v -> {
Toast.makeText(context, file.getName(), Toast.LENGTH_LONG).show();
return true;
});
// Format and set file date
Date lastModified = new Date(file.lastModified());
String formattedDate = DateFormat.format("MMM dd, yyyy hh:mm a", lastModified).toString();
holder.fileDateText.setText(context.getString(R.string.dumpstate_date_format, formattedDate));
// Format and set file size
String fileSize = Formatter.formatFileSize(context, file.length());
holder.fileSizeText.setText(context.getString(R.string.dumpstate_size_format, fileSize));
// Set checkbox state
holder.checkBox.setChecked(position == selectedPosition);
// Set click listener for the whole item
holder.itemView.setOnClickListener(v -> {
int previousSelected = selectedPosition;
selectedPosition = holder.getAdapterPosition();
// Update previous selected item
if (previousSelected >= 0) {
notifyItemChanged(previousSelected);
}
// Update newly selected item
notifyItemChanged(selectedPosition);
// Notify listener
if (listener != null) {
listener.onItemSelected(file);
}
});
}
@Override
public int getItemCount() {
return files.size();
}
/**
* ViewHolder for dumpstate file items
*/
static class ViewHolder extends RecyclerView.ViewHolder {
final CheckBox checkBox;
final TextView fileNameText;
final TextView fileDateText;
final TextView fileSizeText;
ViewHolder(View itemView) {
super(itemView);
checkBox = itemView.findViewById(R.id.checkBox);
fileNameText = itemView.findViewById(R.id.fileNameText);
fileDateText = itemView.findViewById(R.id.fileDateText);
fileSizeText = itemView.findViewById(R.id.fileSizeText);
}
}
/**
* Interface for handling item selection
*/
public interface OnItemSelectedListener {
void onItemSelected(File file);
}
}

View File

@@ -13,7 +13,7 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.concurrent.Callable; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -41,6 +41,15 @@ public class FileController {
return FileUtil.findMostRecentDumpStateFile(); return FileUtil.findMostRecentDumpStateFile();
} }
/**
* Find all dumpstate files
*
* @return Array of dumpstate files
*/
public static File[] getAllDumpStateFiles() {
return FileUtil.findAllDumpStateFiles();
}
/** /**
* Prepare a file for upload - ensures the file is zipped * Prepare a file for upload - ensures the file is zipped
* *
@@ -91,6 +100,47 @@ public class FileController {
} }
} }
/**
* Prepare multiple files for upload by zipping them into a single file
*
* @param files The list of files to prepare
* @param context The context
* @param callback Callback for progress updates and completion
*/
public static void prepareMultipleFilesForUpload(List<File> files, Context context, FilePrepareCallback callback) {
if (files == null || files.isEmpty()) {
callback.onError(new IOException("No files provided"));
return;
}
// Create a unique filename for the zip
String zipFileName = "multiple_files_" + System.currentTimeMillis() + FileConstants.ZIP_EXTENSION;
try {
// Use cache directory for temporary zip storage
ZipUtil.zipFiles(files, context.getCacheDir(), zipFileName, new ZipUtil.ZipProgressListener() {
@Override
public void onProgress(long bytesProcessed, long totalBytes) {
int progress = (int) ((bytesProcessed * 100) / Math.max(totalBytes, 1)); // Avoid division by zero
callback.onProgress(progress);
}
@Override
public void onComplete(File zipFile) {
callback.onSuccess(zipFile);
}
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
} catch (IOException e) {
Log.e(TAG, "Error preparing multiple files for upload", e);
callback.onError(e);
}
}
/** /**
* Prepare a file for upload asynchronously * Prepare a file for upload asynchronously
* *
@@ -124,6 +174,32 @@ public class FileController {
}, Executors.newSingleThreadExecutor()); }, Executors.newSingleThreadExecutor());
} }
/**
* Prepare multiple files for upload asynchronously
*
* @param files The list of files to prepare
* @param context The context
* @return CompletableFuture containing the prepared file
*/
public static CompletableFuture<File> prepareMultipleFilesForUploadAsync(List<File> files, Context context) {
return CompletableFuture.supplyAsync(() -> {
try {
if (files == null || files.isEmpty()) {
throw new IOException("No files provided");
}
// Create a unique filename for the zip
String zipFileName = "multiple_files_" + System.currentTimeMillis() + FileConstants.ZIP_EXTENSION;
// Use cache directory for temporary zip storage
return ZipUtil.zipFiles(files, context.getCacheDir(), zipFileName, null);
} catch (IOException e) {
Log.e(TAG, "Error preparing multiple files for upload", e);
throw new RuntimeException(e);
}
}, Executors.newSingleThreadExecutor());
}
/** /**
* Save a content URI to a temporary file * Save a content URI to a temporary file
* *

View File

@@ -16,38 +16,47 @@ import java.util.Comparator;
public class FileUtil { public class FileUtil {
private static final String TAG = AppConstants.APP_TAG + "FileUtil"; private static final String TAG = AppConstants.APP_TAG + "FileUtil";
/**
* Finds all dumpstate files in the logs directory
* @return Array of dumpstate files or empty array if none found
*/
public static File[] findAllDumpStateFiles() {
File dir = new File(Environment.getExternalStorageDirectory(), FileConstants.LOG_DIRECTORY);
return findAllFilesByPattern(dir, AppConstants.DUMPSTATE_FILE_PATTERN);
}
/** /**
* Finds the most recent dumpstate file in the logs directory * Finds the most recent dumpstate file in the logs directory
* @return The most recent dumpstate file or null if none found * @return The most recent dumpstate file or null if none found
*/ */
public static File findMostRecentDumpStateFile() { public static File findMostRecentDumpStateFile() {
File dir = new File(Environment.getExternalStorageDirectory(), FileConstants.LOG_DIRECTORY); File[] files = findAllDumpStateFiles();
return findMostRecentFileByPattern(dir, AppConstants.DUMPSTATE_FILE_PATTERN); return files.length > 0 ? files[0] : null;
} }
/** /**
* Finds the most recent file in a directory matching the given pattern * Finds all files in a directory matching the given pattern
* *
* @param dir The directory to search * @param dir The directory to search
* @param pattern The regex pattern to match filenames * @param pattern The regex pattern to match filenames
* @return The most recent matching file or null if none found * @return Array of matching files or empty array if none found
*/ */
public static File findMostRecentFileByPattern(File dir, String pattern) { public static File[] findAllFilesByPattern(File dir, String pattern) {
if (dir == null || !dir.exists() || !dir.isDirectory()) { if (dir == null || !dir.exists() || !dir.isDirectory()) {
Log.e(TAG, "Invalid directory: " + (dir != null ? dir.getAbsolutePath() : "null")); Log.e(TAG, "Invalid directory: " + (dir != null ? dir.getAbsolutePath() : "null"));
return null; return new File[0];
} }
File[] files = dir.listFiles((d, name) -> name.matches(pattern)); File[] files = dir.listFiles((d, name) -> name.matches(pattern));
if (files == null || files.length == 0) { if (files == null) {
Log.d(TAG, "No files matching pattern '" + pattern + "' found in " + dir.getAbsolutePath()); Log.d(TAG, "No files matching pattern '" + pattern + "' found in " + dir.getAbsolutePath());
return null; return new File[0];
} }
return Arrays.stream(files) // Sort files by last modified date (newest first)
.max(Comparator.comparingLong(File::lastModified)) Arrays.sort(files, Comparator.comparingLong(File::lastModified).reversed());
.orElse(null); return files;
} }
/** /**

View File

@@ -11,6 +11,7 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -121,4 +122,107 @@ public class ZipUtil {
throw e; throw e;
} }
} }
/**
* Compresses multiple files into a single zip archive
*
* @param sourceFiles List of files to compress
* @param outputDirectory Directory to store the zip file
* @param zipFileName Name of the zip file to create
* @param listener A listener for tracking progress (can be null)
* @return The compressed zip file
* @throws IOException If compression fails
*/
public static File zipFiles(List<File> sourceFiles, File outputDirectory, String zipFileName,
ZipProgressListener listener) throws IOException {
if (sourceFiles == null || sourceFiles.isEmpty()) {
String msg = "No source files provided for zipping";
Log.e(TAG, msg);
throw new IOException(msg);
}
// Create output directory if it doesn't exist
if (!outputDirectory.exists()) {
if (!outputDirectory.mkdirs()) {
throw new IOException("Could not create output directory: " + outputDirectory.getAbsolutePath());
}
}
// Ensure the zip extension
if (!zipFileName.toLowerCase().endsWith(FileConstants.ZIP_EXTENSION)) {
zipFileName += FileConstants.ZIP_EXTENSION;
}
// Create a zip file in the output directory
File zipFile = new File(outputDirectory, zipFileName);
Log.d(TAG, "Creating multi-file zip: " + zipFile.getAbsolutePath() + " with " + sourceFiles.size() + " files");
// Calculate total size for progress reporting
long totalBytes = 0;
for (File file : sourceFiles) {
if (file.exists()) {
totalBytes += file.length();
}
}
try (FileOutputStream fos = new FileOutputStream(zipFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
ZipOutputStream zos = new ZipOutputStream(bos)) {
byte[] buffer = new byte[BUFFER_SIZE];
long bytesProcessed = 0;
for (File file : sourceFiles) {
if (!file.exists() || !file.isFile()) {
Log.w(TAG, "Skipping non-existent or non-file: " + file.getAbsolutePath());
continue;
}
try (FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis)) {
ZipEntry zipEntry = new ZipEntry(file.getName());
zipEntry.setSize(file.length());
zos.putNextEntry(zipEntry);
int len;
while ((len = bis.read(buffer)) > 0) {
zos.write(buffer, 0, len);
bytesProcessed += len;
if (listener != null) {
listener.onProgress(bytesProcessed, totalBytes);
}
}
zos.closeEntry();
}
}
if (listener != null) {
listener.onComplete(zipFile);
}
Log.d(TAG, "Successfully created multi-file zip: " + zipFile.getName());
return zipFile;
} catch (IOException e) {
Log.e(TAG, "Error creating multi-file zip", e);
// Clean up the partial zip file if it exists
if (zipFile.exists()) {
boolean deleted = zipFile.delete();
if (!deleted) {
Log.w(TAG, "Failed to delete partial zip file: " + zipFile.getAbsolutePath());
}
}
if (listener != null) {
listener.onError(e);
}
throw e;
}
}
} }

View File

@@ -0,0 +1,112 @@
package com.mattintech.simplelogupload.view;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.mattintech.simplelogupload.R;
import com.mattintech.simplelogupload.adapter.DumpstateFileAdapter;
import java.io.File;
import java.util.Arrays;
import java.util.List;
/**
* Dialog for selecting a dumpstate file from multiple options
*/
public class DumpstateSelectionDialog extends Dialog {
private final List<File> dumpstateFiles;
private final OnDumpstateSelectedListener listener;
private TextView titleText;
private RecyclerView recyclerView;
private TextView emptyView;
private Button selectButton;
private Button cancelButton;
private DumpstateFileAdapter adapter;
/**
* Interface for handling dumpstate selection
*/
public interface OnDumpstateSelectedListener {
void onDumpstateSelected(File dumpstateFile);
}
/**
* Constructor
*
* @param context The context
* @param dumpstateFiles List of dumpstate files
* @param listener Listener for selection events
*/
public DumpstateSelectionDialog(@NonNull Context context,
File[] dumpstateFiles,
OnDumpstateSelectedListener listener) {
super(context);
this.dumpstateFiles = Arrays.asList(dumpstateFiles);
this.listener = listener;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.dialog_dumpstate_selection);
// Initialize views
titleText = findViewById(R.id.titleText);
recyclerView = findViewById(R.id.dumpstateRecyclerView);
emptyView = findViewById(R.id.emptyView);
selectButton = findViewById(R.id.selectButton);
cancelButton = findViewById(R.id.cancelButton);
// Update title with file count
titleText.setText(getContext().getString(R.string.dumpstate_selection_subtitle, dumpstateFiles.size()));
// Set up RecyclerView
setupRecyclerView();
// Set up buttons
cancelButton.setOnClickListener(v -> dismiss());
selectButton.setOnClickListener(v -> {
File selectedFile = adapter.getSelectedFile();
if (selectedFile != null && listener != null) {
listener.onDumpstateSelected(selectedFile);
dismiss();
}
});
// Show empty view if no files
if (dumpstateFiles.isEmpty()) {
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
} else {
recyclerView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
}
}
/**
* Set up the RecyclerView with the adapter
*/
private void setupRecyclerView() {
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
adapter = new DumpstateFileAdapter(getContext(), dumpstateFiles);
recyclerView.setAdapter(adapter);
// Listen for item selection to enable/disable select button
adapter.setOnItemSelectedListener(file -> {
selectButton.setEnabled(true);
});
}
}

View File

@@ -5,6 +5,8 @@ import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView; import androidx.cardview.widget.CardView;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
@@ -17,11 +19,13 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.mattintech.simplelogupload.R; import com.mattintech.simplelogupload.R;
import com.mattintech.simplelogupload.adapter.DumpstateAdapter;
import com.mattintech.simplelogupload.constant.AppConstants; import com.mattintech.simplelogupload.constant.AppConstants;
import com.mattintech.simplelogupload.controller.FileController; import com.mattintech.simplelogupload.controller.FileController;
import com.mattintech.simplelogupload.controller.ShareIntentHandler; import com.mattintech.simplelogupload.controller.ShareIntentHandler;
@@ -31,6 +35,8 @@ import com.mattintech.simplelogupload.util.PermissionUtil;
import com.mattintech.simplelogupload.util.PreferencesUtil; import com.mattintech.simplelogupload.util.PreferencesUtil;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
/** /**
@@ -47,6 +53,14 @@ public class MainActivity extends AppCompatActivity {
private ProgressBar progressBar; private ProgressBar progressBar;
private CardView selectedFileCard; private CardView selectedFileCard;
// Dumpstate multi-select components
private RecyclerView dumpstateRecyclerView;
private Button toggleDumpstateListButton;
private Button selectAllButton;
private Button clearSelectionsButton;
private LinearLayout multiSelectButtonsLayout;
private DumpstateAdapter dumpstateAdapter;
private File selectedFile; private File selectedFile;
private File dumpstateFile; private File dumpstateFile;
@@ -57,6 +71,9 @@ public class MainActivity extends AppCompatActivity {
// Activity result launcher for file picking // Activity result launcher for file picking
private ActivityResultLauncher<String> filePickerLauncher; private ActivityResultLauncher<String> filePickerLauncher;
// Flag to track if dumpstate list is expanded
private boolean isDumpstateListExpanded = false;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -85,10 +102,23 @@ public class MainActivity extends AppCompatActivity {
progressBar = findViewById(R.id.progressBar); progressBar = findViewById(R.id.progressBar);
selectedFileCard = findViewById(R.id.selectedFileCard); selectedFileCard = findViewById(R.id.selectedFileCard);
// Initialize multi-select UI elements
dumpstateRecyclerView = findViewById(R.id.dumpstateRecyclerView);
toggleDumpstateListButton = findViewById(R.id.toggleDumpstateListButton);
selectAllButton = findViewById(R.id.selectAllButton);
clearSelectionsButton = findViewById(R.id.clearSelectionsButton);
multiSelectButtonsLayout = findViewById(R.id.multiSelectButtonsLayout);
// Set up RecyclerView
dumpstateRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// Set up button click listeners // Set up button click listeners
selectFileButton.setOnClickListener(v -> openFilePicker()); selectFileButton.setOnClickListener(v -> openFilePicker());
uploadDumpstateButton.setOnClickListener(v -> uploadDumpstate()); uploadDumpstateButton.setOnClickListener(v -> uploadDumpstate());
uploadSelectedFileButton.setOnClickListener(v -> uploadSelectedFile()); uploadSelectedFileButton.setOnClickListener(v -> uploadSelectedFile());
toggleDumpstateListButton.setOnClickListener(v -> toggleDumpstateList());
selectAllButton.setOnClickListener(v -> selectAllDumpstates());
clearSelectionsButton.setOnClickListener(v -> clearDumpstateSelections());
// Initialize file picker launcher // Initialize file picker launcher
filePickerLauncher = registerForActivityResult( filePickerLauncher = registerForActivityResult(
@@ -109,6 +139,149 @@ public class MainActivity extends AppCompatActivity {
handleIntent(getIntent()); handleIntent(getIntent());
} }
/**
* Toggle the visibility of the dumpstate list
*/
private void toggleDumpstateList() {
isDumpstateListExpanded = !isDumpstateListExpanded;
if (isDumpstateListExpanded) {
// Show the list
loadDumpstateFiles();
dumpstateRecyclerView.setVisibility(View.VISIBLE);
multiSelectButtonsLayout.setVisibility(View.VISIBLE);
toggleDumpstateListButton.setText(R.string.hide_dumpstates);
// Change the upload button to handle multiple files
updateUploadButtonForMultiSelect();
} else {
// Hide the list
dumpstateRecyclerView.setVisibility(View.GONE);
multiSelectButtonsLayout.setVisibility(View.GONE);
toggleDumpstateListButton.setText(R.string.show_dumpstates);
// Restore normal upload button
uploadDumpstateButton.setText(R.string.upload_dumpstate_button);
uploadDumpstateButton.setOnClickListener(v -> uploadDumpstate());
}
}
/**
* Load dumpstate files into the RecyclerView
*/
private void loadDumpstateFiles() {
File[] dumpstateFiles = FileController.getAllDumpStateFiles();
if (dumpstateFiles.length > 0) {
List<File> fileList = new ArrayList<>(Arrays.asList(dumpstateFiles));
if (dumpstateAdapter == null) {
dumpstateAdapter = new DumpstateAdapter(this, fileList);
dumpstateAdapter.setOnSelectionChangedListener(this::onDumpstateSelectionChanged);
dumpstateRecyclerView.setAdapter(dumpstateAdapter);
} else {
// Just update the adapter with new files
dumpstateAdapter = new DumpstateAdapter(this, fileList);
dumpstateAdapter.setOnSelectionChangedListener(this::onDumpstateSelectionChanged);
dumpstateRecyclerView.setAdapter(dumpstateAdapter);
}
}
}
/**
* Handle selection changes in the dumpstate list
*
* @param selectedCount The number of selected items
*/
private void onDumpstateSelectionChanged(int selectedCount) {
// Update the upload button text
if (selectedCount > 0) {
uploadDumpstateButton.setText(getString(R.string.upload_multiple_dumpstates, selectedCount));
uploadDumpstateButton.setEnabled(true);
} else {
uploadDumpstateButton.setText(R.string.upload_dumpstate_button);
uploadDumpstateButton.setEnabled(false);
}
}
/**
* Update the upload button to handle multiple files
*/
private void updateUploadButtonForMultiSelect() {
uploadDumpstateButton.setOnClickListener(v -> uploadSelectedDumpstates());
// Initially disable until files are selected
uploadDumpstateButton.setEnabled(dumpstateAdapter != null && dumpstateAdapter.getSelectedCount() > 0);
// Update button text if there are already selections
if (dumpstateAdapter != null && dumpstateAdapter.getSelectedCount() > 0) {
uploadDumpstateButton.setText(getString(R.string.upload_multiple_dumpstates,
dumpstateAdapter.getSelectedCount()));
} else {
uploadDumpstateButton.setText(R.string.upload_dumpstate_button);
}
}
/**
* Select all dumpstate files
*/
private void selectAllDumpstates() {
if (dumpstateAdapter != null) {
dumpstateAdapter.selectAll();
}
}
/**
* Clear all dumpstate selections
*/
private void clearDumpstateSelections() {
if (dumpstateAdapter != null) {
dumpstateAdapter.clearSelections();
}
}
/**
* Upload selected dumpstate files
*/
private void uploadSelectedDumpstates() {
if (dumpstateAdapter == null || dumpstateAdapter.getSelectedCount() == 0) {
Toast.makeText(this, R.string.no_dumpstates_selected, Toast.LENGTH_SHORT).show();
return;
}
List<File> selectedFiles = dumpstateAdapter.getSelectedFiles();
int fileCount = selectedFiles.size();
// Update UI
progressBar.setVisibility(View.VISIBLE);
uploadDumpstateButton.setEnabled(false);
dumpstateStatusText.setText(getString(R.string.preparing_multiple_files, fileCount));
// Prepare and upload multiple files
FileController.prepareMultipleFilesForUploadAsync(selectedFiles, this)
.thenAccept(zipFile -> {
Log.d(TAG, "Multiple files zipped successfully: " + zipFile.getAbsolutePath());
runOnUiThread(() -> {
dumpstateStatusText.setText(getString(R.string.uploading_multiple_files, fileCount));
});
// Upload the zip file
startUploadService(zipFile);
})
.exceptionally(ex -> {
Log.e(TAG, "Error preparing multiple files", ex);
runOnUiThread(() -> {
progressBar.setVisibility(View.GONE);
uploadDumpstateButton.setEnabled(true);
Toast.makeText(MainActivity.this,
getString(R.string.file_error, ex.getMessage()),
Toast.LENGTH_LONG).show();
});
return null;
});
}
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
super.onNewIntent(intent); super.onNewIntent(intent);
@@ -229,7 +402,37 @@ public class MainActivity extends AppCompatActivity {
* Upload the dumpstate file * Upload the dumpstate file
*/ */
private void uploadDumpstate() { private void uploadDumpstate() {
if (dumpstateFile == null || !dumpstateFile.exists()) { // Get all dumpstate files
File[] dumpstateFiles = FileController.getAllDumpStateFiles();
if (dumpstateFiles.length == 0) {
Toast.makeText(this, R.string.no_dumpstate_found, Toast.LENGTH_SHORT).show();
return;
}
if (dumpstateFiles.length == 1) {
// Only one dumpstate file, use it directly
dumpstateFile = dumpstateFiles[0];
startDumpstateUpload(dumpstateFile);
} else {
// Multiple dumpstate files are available, suggest expanding the list
if (!isDumpstateListExpanded) {
toggleDumpstateList();
} else {
// If list is already expanded, use the first file
dumpstateFile = dumpstateFiles[0];
startDumpstateUpload(dumpstateFile);
}
}
}
/**
* Start uploading the selected dumpstate file
*
* @param file The dumpstate file to upload
*/
private void startDumpstateUpload(File file) {
if (file == null || !file.exists()) {
Toast.makeText(this, R.string.no_dumpstate_found, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.no_dumpstate_found, Toast.LENGTH_SHORT).show();
return; return;
} }
@@ -237,10 +440,10 @@ public class MainActivity extends AppCompatActivity {
// Update UI // Update UI
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
uploadDumpstateButton.setEnabled(false); uploadDumpstateButton.setEnabled(false);
dumpstateStatusText.setText(getString(R.string.uploading_file, dumpstateFile.getName())); dumpstateStatusText.setText(getString(R.string.uploading_file, file.getName()));
// Start the upload service // Start the upload service
startUploadService(dumpstateFile); startUploadService(file);
} }
/** /**
@@ -302,9 +505,41 @@ public class MainActivity extends AppCompatActivity {
* Reset the UI after upload completion or error * Reset the UI after upload completion or error
*/ */
private void resetUI() { private void resetUI() {
dumpstateStatusText.setText(dumpstateFile != null ? // Get all dumpstate files
getString(R.string.dumpstate_found, dumpstateFile.getName()) : File[] dumpstateFiles = FileController.getAllDumpStateFiles();
getString(R.string.no_dumpstate_found));
if (dumpstateFiles.length > 0) {
if (dumpstateFiles.length == 1) {
// Only one dumpstate file
dumpstateFile = dumpstateFiles[0];
dumpstateStatusText.setText(getString(R.string.dumpstate_found, dumpstateFile.getName()));
} else {
// Multiple dumpstate files
dumpstateStatusText.setText(getString(R.string.multiple_dumpstates_found) +
" (" + dumpstateFiles.length + ")");
// If we have a specific file selected, show it
if (dumpstateFile != null && dumpstateFile.exists()) {
dumpstateStatusText.append("\n" + dumpstateFile.getName());
} else {
// Otherwise show the most recent one
dumpstateFile = dumpstateFiles[0];
dumpstateStatusText.append("\n" + dumpstateFile.getName());
}
}
uploadDumpstateButton.setEnabled(true);
// Update multiselect if active
if (isDumpstateListExpanded && dumpstateAdapter != null) {
loadDumpstateFiles();
updateUploadButtonForMultiSelect();
}
} else {
dumpstateFile = null;
dumpstateStatusText.setText(R.string.no_dumpstate_found);
uploadDumpstateButton.setEnabled(false);
}
if (selectedFile != null) { if (selectedFile != null) {
selectedFileText.setText(getString(R.string.selected_file, selectedFile.getName())); selectedFileText.setText(getString(R.string.selected_file, selectedFile.getName()));
} }
@@ -368,16 +603,33 @@ public class MainActivity extends AppCompatActivity {
*/ */
private void checkForDumpStateAndUpdateUI() { private void checkForDumpStateAndUpdateUI() {
// Look for dumpstate files // Look for dumpstate files
File mostRecentDumpStateFile = FileController.getMostRecentDumpStateFile(); File[] dumpstateFiles = FileController.getAllDumpStateFiles();
if (mostRecentDumpStateFile != null) { if (dumpstateFiles.length > 0) {
dumpstateFile = mostRecentDumpStateFile; if (dumpstateFiles.length == 1) {
dumpstateStatusText.setText(getString(R.string.dumpstate_found, mostRecentDumpStateFile.getName())); // Only one dumpstate file found, use it directly
uploadDumpstateButton.setEnabled(true); dumpstateFile = dumpstateFiles[0];
dumpstateStatusText.setText(getString(R.string.dumpstate_found, dumpstateFile.getName()));
uploadDumpstateButton.setEnabled(true);
} else {
// Multiple dumpstate files found
dumpstateStatusText.setText(getString(R.string.multiple_dumpstates_found) +
" (" + dumpstateFiles.length + ")");
// Set the most recent one as default
dumpstateFile = dumpstateFiles[0];
// Show the dumpstate name in a new line
dumpstateStatusText.append("\n" + dumpstateFile.getName());
uploadDumpstateButton.setEnabled(true);
// Update toggle button visibility
toggleDumpstateListButton.setVisibility(View.VISIBLE);
}
} else { } else {
dumpstateFile = null; dumpstateFile = null;
dumpstateStatusText.setText(R.string.no_dumpstate_found); dumpstateStatusText.setText(R.string.no_dumpstate_found);
uploadDumpstateButton.setEnabled(false); uploadDumpstateButton.setEnabled(false);
toggleDumpstateListButton.setVisibility(View.GONE);
} }
} }

View File

@@ -84,13 +84,64 @@
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold"/> android:textStyle="bold"/>
<TextView <LinearLayout
android:id="@+id/dumpstateStatusText" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/dumpstateStatusText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/looking_for_dumpstate"
android:textSize="16sp"
android:maxLines="3"
android:ellipsize="start"/>
<Button
android:id="@+id/toggleDumpstateListButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show_dumpstates"
android:textSize="12sp"
style="@style/Widget.AppCompat.Button.Small" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/dumpstateRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/looking_for_dumpstate" android:visibility="gone"/>
android:textSize="16sp"/>
<LinearLayout
android:id="@+id/multiSelectButtonsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:visibility="gone">
<Button
android:id="@+id/selectAllButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/select_all"
android:textSize="12sp"
style="@style/Widget.AppCompat.Button.Borderless.Colored" />
<Button
android:id="@+id/clearSelectionsButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/clear_selections"
android:textSize="12sp"
style="@style/Widget.AppCompat.Button.Borderless.Colored" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dumpstate_selection_title"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/dumpstateRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="300dp"/>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/no_dumpstate_found"
android:gravity="center"
android:visibility="gone"
android:padding="16dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="16dp">
<Button
android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
style="@style/Widget.AppCompat.Button.Borderless"/>
<Button
android:id="@+id/selectButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_button"
android:enabled="false"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="?attr/selectableItemBackground">
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/fileNameText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="15sp"
android:textStyle="bold"
android:ellipsize="start"
android:singleLine="true"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/checkBox"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/fileDateText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/checkBox"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/fileNameText"/>
<TextView
android:id="@+id/fileSizeText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/checkBox"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/fileDateText"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -37,4 +37,23 @@
<!-- Permissions --> <!-- Permissions -->
<string name="permission_storage_required">Storage permission is required</string> <string name="permission_storage_required">Storage permission is required</string>
<string name="permission_not_granted">Permission not granted!</string> <string name="permission_not_granted">Permission not granted!</string>
<!-- Dumpstate Selection Dialog -->
<string name="dumpstate_selection_title">Select a Dumpstate File</string>
<string name="dumpstate_selection_subtitle">Found %1$d dumpstate files</string>
<string name="select_button">Select</string>
<string name="dumpstate_date_format">Created: %1$s</string>
<string name="dumpstate_size_format">Size: %1$s</string>
<string name="multiple_dumpstates_found">Multiple dumpstate files found</string>
<string name="choose_dumpstate">Choose a dumpstate file</string>
<!-- Multi-select Dumpstate -->
<string name="show_dumpstates">Show Files</string>
<string name="hide_dumpstates">Hide Files</string>
<string name="select_all">Select All</string>
<string name="clear_selections">Clear All</string>
<string name="upload_multiple_dumpstates">Upload Selected Files (%1$d)</string>
<string name="no_dumpstates_selected">No files selected</string>
<string name="preparing_multiple_files">Preparing %1$d files for upload…</string>
<string name="uploading_multiple_files">Uploading %1$d files as one package</string>
</resources> </resources>