• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jiangxincode / ApkToolBoxGUI / #1428

21 Apr 2026 03:41PM UTC coverage: 3.42% (-0.03%) from 3.452%
#1428

push

jiangxincode
feat: add export to Excel functionality for result panels

- Add ExcelExporter utility class with XSSFWorkbook-based .xlsx export
- Add right-click context menu 'Export to Excel' in PdfStatPanel
- Add right-click context menu 'Export to Excel' in PdfFinderPanel
- Add right-click context menu 'Export to Excel' in WordStatPanel
- Add right-click context menu 'Export to Excel' in DuplicateSearchPanel

0 of 69 new or added lines in 5 files covered. (0.0%)

4 existing lines in 4 files now uncovered.

250 of 7311 relevant lines covered (3.42%)

0.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/main/java/edu/jiangxin/apktoolbox/pdf/finder/PdfFinderPanel.java
1
package edu.jiangxin.apktoolbox.pdf.finder;
2

3
import edu.jiangxin.apktoolbox.pdf.PdfUtils;
4
import edu.jiangxin.apktoolbox.swing.extend.EasyPanel;
5
import edu.jiangxin.apktoolbox.swing.extend.FileListPanel;
6
import edu.jiangxin.apktoolbox.utils.Constants;
7
import edu.jiangxin.apktoolbox.utils.DateUtils;
8
import edu.jiangxin.apktoolbox.utils.ExcelExporter;
9
import edu.jiangxin.apktoolbox.utils.FileUtils;
10
import edu.jiangxin.apktoolbox.utils.RevealFileUtils;
11

12
import javax.swing.*;
13
import javax.swing.table.DefaultTableModel;
14
import java.awt.*;
15
import java.awt.event.*;
16
import java.io.File;
17
import java.io.Serial;
18
import java.util.*;
19
import java.util.List;
20
import java.util.concurrent.ExecutorService;
21
import java.util.concurrent.Executors;
22
import java.util.concurrent.Future;
23
import java.util.concurrent.atomic.AtomicInteger;
24

25
public class PdfFinderPanel extends EasyPanel {
×
26

27
    @Serial
28
    private static final long serialVersionUID = 1L;
29

30
    private JTabbedPane tabbedPane;
31

32
    private JPanel mainPanel;
33

34
    private FileListPanel fileListPanel;
35

36
    private JRadioButton scannedRadioButton;
37

38
    private JRadioButton encryptedRadioButton;
39

40
    private JRadioButton nonOutlineRadioButton;
41

42
    private JRadioButton hasAnnotationsRadioButton;
43

44
    private JSpinner thresholdSpinner;
45

46
    private JCheckBox isRecursiveSearched;
47

48
    private JPanel resultPanel;
49

50
    private JTable resultTable;
51

52
    private DefaultTableModel resultTableModel;
53

54
    private JButton searchButton;
55
    private JButton cancelButton;
56

57
    private JProgressBar progressBar;
58

59
    private JMenuItem openDirMenuItem;
60

61
    private JMenuItem copyFilesMenuItem;
62

63
    private transient SearchThread searchThread;
64

65
    private transient final List<File> resultFileList = new ArrayList<>();
×
66

67

68
    @Override
69
    public void initUI() {
70
        tabbedPane = new JTabbedPane();
×
71
        add(tabbedPane);
×
72

73
        createMainPanel();
×
74
        tabbedPane.addTab("Option", null, mainPanel, "Show Search Options");
×
75

76
        createResultPanel();
×
77
        tabbedPane.addTab("Result", null, resultPanel, "Show Search Result");
×
78
    }
×
79

80
    private void createMainPanel() {
81
        mainPanel = new JPanel();
×
82
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
×
83

84
        fileListPanel = new FileListPanel();
×
85
        fileListPanel.initialize();
×
86

87
        JPanel checkOptionPanel = new JPanel();
×
88
        checkOptionPanel.setLayout(new BoxLayout(checkOptionPanel, BoxLayout.X_AXIS));
×
89
        checkOptionPanel.setBorder(BorderFactory.createTitledBorder("Check Options"));
×
90

91
        ButtonGroup buttonGroup = new ButtonGroup();
×
92
        ItemListener itemListener = new RadioButtonItemListener();
×
93

94
        scannedRadioButton = new JRadioButton("查找扫描的PDF文件");
×
95
        scannedRadioButton.setSelected(true);
×
96
        scannedRadioButton.addItemListener(itemListener);
×
97
        buttonGroup.add(scannedRadioButton);
×
98

99
        encryptedRadioButton = new JRadioButton("查找加密的PDF文件");
×
100
        encryptedRadioButton.addItemListener(itemListener);
×
101
        buttonGroup.add(encryptedRadioButton);
×
102

103
        nonOutlineRadioButton = new JRadioButton("查找没有目录的PDF文件");
×
104
        nonOutlineRadioButton.addItemListener(itemListener);
×
105
        buttonGroup.add(nonOutlineRadioButton);
×
106

107
        hasAnnotationsRadioButton = new JRadioButton("查找有注释的PDF文件");
×
108
        hasAnnotationsRadioButton.addItemListener(itemListener);
×
109
        buttonGroup.add(hasAnnotationsRadioButton);
×
110

111
        JPanel typePanel = new JPanel();
×
112
        typePanel.setLayout(new FlowLayout(FlowLayout.LEFT,10,3));
×
113
        typePanel.add(scannedRadioButton);
×
114
        typePanel.add(encryptedRadioButton);
×
115
        typePanel.add(nonOutlineRadioButton);
×
116
        typePanel.add(hasAnnotationsRadioButton);
×
117

118
        JLabel thresholdLabel = new JLabel("Threshold: ");
×
119
        thresholdSpinner = new JSpinner();
×
120
        thresholdSpinner.setModel(new SpinnerNumberModel(1, 0, 100, 1));
×
121

122
        checkOptionPanel.add(typePanel);
×
123
        checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
×
124
        checkOptionPanel.add(thresholdLabel);
×
125
        checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
×
126
        checkOptionPanel.add(thresholdSpinner);
×
127
        checkOptionPanel.add(Box.createHorizontalGlue());
×
128

129
        JPanel searchOptionPanel = new JPanel();
×
130
        searchOptionPanel.setLayout(new BoxLayout(searchOptionPanel, BoxLayout.X_AXIS));
×
131
        searchOptionPanel.setBorder(BorderFactory.createTitledBorder("Search Options"));
×
132

133
        isRecursiveSearched = new JCheckBox("Recursive");
×
134
        isRecursiveSearched.setSelected(true);
×
135
        searchOptionPanel.add(isRecursiveSearched);
×
136
        searchOptionPanel.add(Box.createHorizontalGlue());
×
137

138
        JPanel operationPanel = new JPanel();
×
139
        operationPanel.setLayout(new BoxLayout(operationPanel, BoxLayout.X_AXIS));
×
140
        operationPanel.setBorder(BorderFactory.createTitledBorder("Operations"));
×
141

142
        JPanel buttonPanel = new JPanel();
×
143
        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
×
144

145
        searchButton = new JButton("Search");
×
146
        cancelButton = new JButton("Cancel");
×
147
        cancelButton.setEnabled(false);
×
148
        searchButton.addActionListener(new OperationButtonActionListener());
×
149
        cancelButton.addActionListener(new OperationButtonActionListener());
×
150
        operationPanel.add(searchButton);
×
151
        operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
×
152
        operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
×
153
        operationPanel.add(cancelButton);
×
154
        operationPanel.add(Box.createHorizontalGlue());
×
155

156
        progressBar = new JProgressBar();
×
157
        progressBar.setStringPainted(true);
×
158
        progressBar.setString("Ready");
×
159

160
        mainPanel.add(fileListPanel);
×
161
        mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
×
162
        mainPanel.add(checkOptionPanel);
×
163
        mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
×
164
        mainPanel.add(searchOptionPanel);
×
165
        mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
×
166
        mainPanel.add(operationPanel);
×
167
        mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
×
168
        mainPanel.add(progressBar);
×
169
    }
×
170

171
    private void createResultPanel() {
172
        resultPanel = new JPanel();
×
173
        resultPanel.setLayout(new BoxLayout(resultPanel, BoxLayout.Y_AXIS));
×
174

175
        resultTableModel = new PdfFilesTableModel(new Vector<>(), PdfFilesConstants.COLUMN_NAMES);
×
176
        resultTable = new JTable(resultTableModel);
×
177

178
        resultTable.setDefaultRenderer(Vector.class, new PdfFilesTableCellRenderer());
×
179

180
        for (int i = 0; i < resultTable.getColumnCount(); i++) {
×
181
            resultTable.getColumn(resultTable.getColumnName(i)).setCellRenderer(new PdfFilesTableCellRenderer());
×
182
        }
183

184
        resultTable.addMouseListener(new MyMouseListener());
×
185

186
        resultTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
×
187

188
        JScrollPane scrollPane = new JScrollPane(resultTable);
×
189
        resultPanel.add(scrollPane);
×
190
    }
×
191

192
    private void processFile(File file) {
193
        if (scannedRadioButton.isSelected()) {
×
194
            int threshold = (Integer) thresholdSpinner.getValue();
×
195
            if (PdfUtils.isScannedPdf(file, threshold)) {
×
196
                resultFileList.add(file);
×
197
            }
198
        } else if (encryptedRadioButton.isSelected()) {
×
199
            if (PdfUtils.isEncryptedPdf(file)) {
×
200
                resultFileList.add(file);
×
201
            }
202
        } else if (nonOutlineRadioButton.isSelected()) {
×
203
            if (PdfUtils.isNonOutlinePdf(file)) {
×
204
                resultFileList.add(file);
×
205
            }
206
        } else if (hasAnnotationsRadioButton.isSelected()) {
×
207
            if (PdfUtils.hasAnnotations(file)) {
×
208
                resultFileList.add(file);
×
209
            }
210
        } else {
211
            logger.error("Invalid option selected");
×
212
        }
213
    }
×
214

215
    class MyMouseListener extends MouseAdapter {
×
216
        @Override
217
        public void mouseReleased(MouseEvent e) {
218
            super.mouseReleased(e);
×
219
            int r = resultTable.rowAtPoint(e.getPoint());
×
220
            if (r >= 0 && r < resultTable.getRowCount()) {
×
221
                if (!resultTable.isRowSelected(r)) {
×
222
                    resultTable.setRowSelectionInterval(r, r);
×
223
                }
224
            } else {
225
                resultTable.clearSelection();
×
226
            }
227
            int[] rowsIndex = resultTable.getSelectedRows();
×
228
            if (rowsIndex == null || rowsIndex.length == 0) {
×
229
                return;
×
230
            }
231
            if (e.isPopupTrigger() && e.getComponent() instanceof JTable) {
×
232
                JPopupMenu popupmenu = new JPopupMenu();
×
233
                MyMenuActionListener menuActionListener = new MyMenuActionListener();
×
234

235
                if (rowsIndex.length == 1) {
×
236
                    openDirMenuItem = new JMenuItem("Open parent folder of this file");
×
237
                    openDirMenuItem.addActionListener(menuActionListener);
×
238
                    popupmenu.add(openDirMenuItem);
×
239
                    popupmenu.addSeparator();
×
240
                }
241

242
                copyFilesMenuItem = new JMenuItem("Copy selected files to...");
×
243
                copyFilesMenuItem.addActionListener(menuActionListener);
×
244
                popupmenu.add(copyFilesMenuItem);
×
245

NEW
246
                JMenuItem exportMenuItem = new JMenuItem("导出到 Excel");
×
NEW
247
                exportMenuItem.addActionListener(ev ->
×
NEW
248
                    ExcelExporter.export(resultTableModel, "pdf_finder_export.xlsx", PdfFinderPanel.this));
×
NEW
249
                popupmenu.add(exportMenuItem);
×
250

UNCOV
251
                popupmenu.show(e.getComponent(), e.getX(), e.getY());
×
252
            }
253
        }
×
254
    }
255

256
    class MyMenuActionListener implements ActionListener {
×
257
        @Override
258
        public void actionPerformed(ActionEvent actionEvent) {
259
            Object source = actionEvent.getSource();
×
260
            if (source.equals(openDirMenuItem)) {
×
261
                onOpenDir();
×
262
            } else if (source.equals(copyFilesMenuItem)) {
×
263
                onCopyFiles();
×
264
            } else {
265
                logger.error("invalid source");
×
266
            }
267
        }
×
268

269
        private void onOpenDir() {
270
            int rowIndex = resultTable.getSelectedRow();
×
271
            String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
×
272
            File parent = new File(parentPath);
×
273
            RevealFileUtils.revealDirectory(parent);
×
274
        }
×
275

276
        private void onCopyFiles() {
277
            int[] selectedRows = resultTable.getSelectedRows();
×
278
            if (selectedRows.length == 0) {
×
279
                JOptionPane.showMessageDialog(PdfFinderPanel.this, "No rows selected", "Error", JOptionPane.ERROR_MESSAGE);
×
280
                return;
×
281
            }
282
            List<File> filesToCopy = new ArrayList<>();
×
283
            for (int rowIndex : selectedRows) {
×
284
                String filePath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_NAME).getModelIndex()).toString();
×
285
                String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
×
286
                File file = new File(parentPath, filePath);
×
287
                filesToCopy.add(file);
×
288
            }
289
            JFileChooser fileChooser = new JFileChooser();
×
290
            fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
×
291
            fileChooser.setDialogTitle("Select Target Directory");
×
292
            int returnValue = fileChooser.showOpenDialog(PdfFinderPanel.this);
×
293
            if (returnValue != JFileChooser.APPROVE_OPTION) {
×
294
                return;
×
295
            }
296
            File targetDir = fileChooser.getSelectedFile();
×
297
            for (File file : filesToCopy) {
×
298
                try {
299
                    org.apache.commons.io.FileUtils.copyFileToDirectory(file, targetDir);
×
300
                } catch (Exception e) {
×
301
                    logger.error("Copy file failed: " + file.getAbsolutePath(), e);
×
302
                }
×
303
            }
×
304
        }
×
305

306

307
    }
308

309
    class OperationButtonActionListener implements ActionListener {
×
310
        @Override
311
        public void actionPerformed(ActionEvent e) {
312
            Object source = e.getSource();
×
313
            if (source.equals(searchButton)) {
×
314
                searchButton.setEnabled(false);
×
315
                cancelButton.setEnabled(true);
×
316
                searchThread = new SearchThread(isRecursiveSearched.isSelected());
×
317
                searchThread.start();
×
318
            } else if (source.equals(cancelButton)) {
×
319
                searchButton.setEnabled(true);
×
320
                cancelButton.setEnabled(false);
×
321
                if (searchThread.isAlive()) {
×
322
                    searchThread.interrupt();
×
323
                    searchThread.executorService.shutdownNow();
×
324
                }
325
            }
326

327
        }
×
328
    }
329

330
    private void showResult() {
331
        SwingUtilities.invokeLater(() -> {
×
332
            int index = 0;
×
333
            for (File file : resultFileList) {
×
334
                index++;
×
335
                Vector<Object> rowData = getRowVector(index, file);
×
336
                resultTableModel.addRow(rowData);
×
337
            }
×
338
            tabbedPane.setSelectedIndex(1);
×
339
        });
×
340
    }
×
341

342
    private Vector<Object> getRowVector(int index, File file) {
343
        Vector<Object> rowData = new Vector<>();
×
344
        rowData.add(index);
×
345
        rowData.add(file.getParent());
×
346
        rowData.add(file.getName());
×
347
        rowData.add(FileUtils.sizeOfInHumanFormat(file));
×
348
        rowData.add(DateUtils.millisecondToHumanFormat(file.lastModified()));
×
349
        return rowData;
×
350
    }
351

352
    class SearchThread extends Thread {
353
        public final ExecutorService executorService;
354
        private final AtomicInteger processedFiles = new AtomicInteger(0);
×
355
        private int totalFiles = 0;
×
356
        private final boolean isRecursiveSearched;
357

358
        public SearchThread(boolean isRecursiveSearched) {
×
359
            super();
×
360
            this.isRecursiveSearched = isRecursiveSearched;
×
361
            this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
×
362

363
            SwingUtilities.invokeLater(() -> {
×
364
                progressBar.setValue(0);
×
365
                progressBar.setString("Starting search...");
×
366
            });
×
367
        }
×
368

369
        @Override
370
        public void run() {
371
            try {
372
                resultFileList.clear();
×
373
                SwingUtilities.invokeLater(() -> resultTableModel.setRowCount(0));
×
374

375
                List<File> fileList = fileListPanel.getFileList();
×
376
                Set<File> fileSet = new TreeSet<>();
×
377
                String[] extensions = new String[]{"pdf", "PDF"};
×
378
                for (File file : fileList) {
×
379
                    fileSet.addAll(FileUtils.listFiles(file, extensions, isRecursiveSearched));
×
380
                }
×
381

382
                List<Future<?>> futures = new ArrayList<>();
×
383
                totalFiles = fileSet.size();
×
384
                updateProgress();
×
385

386
                for (File file : fileSet) {
×
387
                    if (currentThread().isInterrupted()) {
×
388
                        return;
×
389
                    }
390
                    futures.add(executorService.submit(() -> {
×
391
                        if (currentThread().isInterrupted()) {
×
392
                            return null;
×
393
                        }
394
                        processFile(file);
×
395
                        incrementProcessedFiles();
×
396
                        return null;
×
397
                    }));
398
                }
×
399

400
                // Wait for all tasks to complete
401
                for (Future<?> future : futures) {
×
402
                    try {
403
                        future.get();
×
404
                    } catch (InterruptedException e) {
×
405
                        logger.error("Search interrupted", e);
×
406
                        currentThread().interrupt(); // Restore interrupted status
×
407
                        return;
×
408
                    }
×
409
                }
×
410

411
                showResult();
×
412
            } catch (Exception e) {
×
413
                logger.error("Search failed", e);
×
414
                SwingUtilities.invokeLater(() -> progressBar.setString("Search failed"));
×
415
            } finally {
416
                executorService.shutdown();
×
417
                SwingUtilities.invokeLater(() -> {
×
418
                    searchButton.setEnabled(true);
×
419
                    cancelButton.setEnabled(false);
×
420
                });
×
421
            }
422
        }
×
423

424
        private void incrementProcessedFiles() {
425
            processedFiles.incrementAndGet();
×
426
            updateProgress();
×
427
        }
×
428

429
        private void updateProgress() {
430
            if (totalFiles > 0) {
×
431
                SwingUtilities.invokeLater(() -> {
×
432
                    int processed = processedFiles.get();
×
433
                    int percentage = (int) (processed * 100.0 / totalFiles);
×
434
                    progressBar.setValue(percentage);
×
435
                    progressBar.setString(String.format("Processing: %d/%d files (%d%%)", processed, totalFiles, percentage));
×
436
                });
×
437
            }
438
        }
×
439
    }
440

441
    class RadioButtonItemListener implements ItemListener {
×
442
        @Override
443
        public void itemStateChanged(ItemEvent e) {
444
            thresholdSpinner.setEnabled(scannedRadioButton.isSelected());
×
445
        }
×
446
    }
447
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc