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
246 JMenuItem exportMenuItem = new JMenuItem("导出到 Excel");
247 exportMenuItem.addActionListener(ev ->
248 ExcelExporter.export(resultTableModel, "pdf_finder_export.xlsx", PdfFinderPanel.this));
249 popupmenu.add(exportMenuItem);
250
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
401 for (Future<?> future : futures) {
402 try {
403 future.get();
404 } catch (InterruptedException e) {
405 logger.error("Search interrupted", e);
406 currentThread().interrupt();
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 }