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.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<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
|
||||
*
|
||||
@@ -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<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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<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.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;
|
||||
|
||||
@@ -58,6 +72,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
// Activity result launcher for file picking
|
||||
private ActivityResultLauncher<String> filePickerLauncher;
|
||||
|
||||
// Flag to track if dumpstate list is expanded
|
||||
private boolean isDumpstateListExpanded = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(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<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
|
||||
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()));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,13 +84,64 @@
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dumpstateStatusText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/looking_for_dumpstate"
|
||||
android:textSize="16sp"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="start"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggleDumpstateListButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/show_dumpstates"
|
||||
android:textSize="12sp"
|
||||
style="@style/Widget.AppCompat.Button.Small" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/dumpstateRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/looking_for_dumpstate"
|
||||
android:textSize="16sp"/>
|
||||
android:visibility="gone"/>
|
||||
|
||||
<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>
|
||||
</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 -->
|
||||
<string name="permission_storage_required">Storage permission is required</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>
|
||||
|
||||
Reference in New Issue
Block a user