View Javadoc
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                 // 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 }