multi file and multi dumpstate support
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -58,6 +72,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) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(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
|
||||||
|
dumpstateFile = dumpstateFiles[0];
|
||||||
|
dumpstateStatusText.setText(getString(R.string.dumpstate_found, dumpstateFile.getName()));
|
||||||
uploadDumpstateButton.setEnabled(true);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,13 +84,64 @@
|
|||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"/>
|
android:textStyle="bold"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="4dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/dumpstateStatusText"
|
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>
|
||||||
|
|
||||||
|
|||||||
53
app/src/main/res/layout/dialog_dumpstate_selection.xml
Normal file
53
app/src/main/res/layout/dialog_dumpstate_selection.xml
Normal 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>
|
||||||
51
app/src/main/res/layout/item_dumpstate_file.xml
Normal file
51
app/src/main/res/layout/item_dumpstate_file.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user