diff --git a/app/src/main/java/com/mattintech/simplelogupload/adapter/DumpstateAdapter.java b/app/src/main/java/com/mattintech/simplelogupload/adapter/DumpstateAdapter.java new file mode 100644 index 0000000..f3dd6e7 --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/adapter/DumpstateAdapter.java @@ -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 { + + private final List files; + private final Context context; + private final Set selectedPositions = new HashSet<>(); + private OnSelectionChangedListener selectionChangedListener; + + /** + * Constructor + * + * @param context The context + * @param files List of dumpstate files + */ + public DumpstateAdapter(Context context, List 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 getSelectedFiles() { + List 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); + } +} diff --git a/app/src/main/java/com/mattintech/simplelogupload/adapter/DumpstateFileAdapter.java b/app/src/main/java/com/mattintech/simplelogupload/adapter/DumpstateFileAdapter.java new file mode 100644 index 0000000..537e7d0 --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/adapter/DumpstateFileAdapter.java @@ -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 { + + private final List 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 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); + } +} diff --git a/app/src/main/java/com/mattintech/simplelogupload/controller/FileController.java b/app/src/main/java/com/mattintech/simplelogupload/controller/FileController.java index 466a687..69a8ec5 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/controller/FileController.java +++ b/app/src/main/java/com/mattintech/simplelogupload/controller/FileController.java @@ -13,7 +13,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.concurrent.Callable; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; @@ -41,6 +41,15 @@ public class FileController { 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 * @@ -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 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 * @@ -124,6 +174,32 @@ public class FileController { }, 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 prepareMultipleFilesForUploadAsync(List 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 * diff --git a/app/src/main/java/com/mattintech/simplelogupload/util/FileUtil.java b/app/src/main/java/com/mattintech/simplelogupload/util/FileUtil.java index c8fba47..62b8389 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/util/FileUtil.java +++ b/app/src/main/java/com/mattintech/simplelogupload/util/FileUtil.java @@ -16,38 +16,47 @@ import java.util.Comparator; public class 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 * @return The most recent dumpstate file or null if none found */ public static File findMostRecentDumpStateFile() { - File dir = new File(Environment.getExternalStorageDirectory(), FileConstants.LOG_DIRECTORY); - return findMostRecentFileByPattern(dir, AppConstants.DUMPSTATE_FILE_PATTERN); + File[] files = findAllDumpStateFiles(); + 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 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()) { 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)); - if (files == null || files.length == 0) { + if (files == null) { Log.d(TAG, "No files matching pattern '" + pattern + "' found in " + dir.getAbsolutePath()); - return null; + return new File[0]; } - return Arrays.stream(files) - .max(Comparator.comparingLong(File::lastModified)) - .orElse(null); + // Sort files by last modified date (newest first) + Arrays.sort(files, Comparator.comparingLong(File::lastModified).reversed()); + return files; } /** diff --git a/app/src/main/java/com/mattintech/simplelogupload/util/ZipUtil.java b/app/src/main/java/com/mattintech/simplelogupload/util/ZipUtil.java index b138581..3e5355d 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/util/ZipUtil.java +++ b/app/src/main/java/com/mattintech/simplelogupload/util/ZipUtil.java @@ -11,6 +11,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -121,4 +122,107 @@ public class ZipUtil { 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 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; + } + } } diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/DumpstateSelectionDialog.java b/app/src/main/java/com/mattintech/simplelogupload/view/DumpstateSelectionDialog.java new file mode 100644 index 0000000..bdaf098 --- /dev/null +++ b/app/src/main/java/com/mattintech/simplelogupload/view/DumpstateSelectionDialog.java @@ -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 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); + }); + } +} diff --git a/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java b/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java index c91d2eb..b699224 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java +++ b/app/src/main/java/com/mattintech/simplelogupload/view/MainActivity.java @@ -5,6 +5,8 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; import androidx.cardview.widget.CardView; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.content.BroadcastReceiver; import android.content.Context; @@ -17,11 +19,13 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; +import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.mattintech.simplelogupload.R; +import com.mattintech.simplelogupload.adapter.DumpstateAdapter; import com.mattintech.simplelogupload.constant.AppConstants; import com.mattintech.simplelogupload.controller.FileController; import com.mattintech.simplelogupload.controller.ShareIntentHandler; @@ -31,6 +35,8 @@ import com.mattintech.simplelogupload.util.PermissionUtil; import com.mattintech.simplelogupload.util.PreferencesUtil; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -47,6 +53,14 @@ public class MainActivity extends AppCompatActivity { private ProgressBar progressBar; 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 dumpstateFile; @@ -57,6 +71,9 @@ public class MainActivity extends AppCompatActivity { // Activity result launcher for file picking private ActivityResultLauncher filePickerLauncher; + + // Flag to track if dumpstate list is expanded + private boolean isDumpstateListExpanded = false; @Override protected void onCreate(Bundle savedInstanceState) { @@ -85,10 +102,23 @@ public class MainActivity extends AppCompatActivity { progressBar = findViewById(R.id.progressBar); 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 selectFileButton.setOnClickListener(v -> openFilePicker()); uploadDumpstateButton.setOnClickListener(v -> uploadDumpstate()); uploadSelectedFileButton.setOnClickListener(v -> uploadSelectedFile()); + toggleDumpstateListButton.setOnClickListener(v -> toggleDumpstateList()); + selectAllButton.setOnClickListener(v -> selectAllDumpstates()); + clearSelectionsButton.setOnClickListener(v -> clearDumpstateSelections()); // Initialize file picker launcher filePickerLauncher = registerForActivityResult( @@ -109,6 +139,149 @@ public class MainActivity extends AppCompatActivity { 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 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 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 protected void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -229,7 +402,37 @@ public class MainActivity extends AppCompatActivity { * Upload the dumpstate file */ 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(); return; } @@ -237,10 +440,10 @@ public class MainActivity extends AppCompatActivity { // Update UI progressBar.setVisibility(View.VISIBLE); 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 - startUploadService(dumpstateFile); + startUploadService(file); } /** @@ -302,9 +505,41 @@ public class MainActivity extends AppCompatActivity { * Reset the UI after upload completion or error */ private void resetUI() { - dumpstateStatusText.setText(dumpstateFile != null ? - getString(R.string.dumpstate_found, dumpstateFile.getName()) : - getString(R.string.no_dumpstate_found)); + // Get all dumpstate files + File[] dumpstateFiles = FileController.getAllDumpStateFiles(); + + 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) { selectedFileText.setText(getString(R.string.selected_file, selectedFile.getName())); } @@ -368,16 +603,33 @@ public class MainActivity extends AppCompatActivity { */ private void checkForDumpStateAndUpdateUI() { // Look for dumpstate files - File mostRecentDumpStateFile = FileController.getMostRecentDumpStateFile(); + File[] dumpstateFiles = FileController.getAllDumpStateFiles(); - if (mostRecentDumpStateFile != null) { - dumpstateFile = mostRecentDumpStateFile; - dumpstateStatusText.setText(getString(R.string.dumpstate_found, mostRecentDumpStateFile.getName())); - uploadDumpstateButton.setEnabled(true); + if (dumpstateFiles.length > 0) { + if (dumpstateFiles.length == 1) { + // Only one dumpstate file found, use it directly + 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 { dumpstateFile = null; dumpstateStatusText.setText(R.string.no_dumpstate_found); uploadDumpstateButton.setEnabled(false); + toggleDumpstateListButton.setVisibility(View.GONE); } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5ee3e96..c3e43da 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -84,13 +84,64 @@ android:textSize="18sp" android:textStyle="bold"/> - + + + +