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.FileUtils;
9   import edu.jiangxin.apktoolbox.utils.RevealFileUtils;
10  
11  import javax.swing.*;
12  import javax.swing.table.DefaultTableModel;
13  import java.awt.*;
14  import java.awt.event.*;
15  import java.io.File;
16  import java.io.Serial;
17  import java.util.*;
18  import java.util.List;
19  import java.util.concurrent.ExecutorService;
20  import java.util.concurrent.Executors;
21  import java.util.concurrent.Future;
22  import java.util.concurrent.atomic.AtomicInteger;
23  
24  public class PdfFinderPanel extends EasyPanel {
25  
26      @Serial
27      private static final long serialVersionUID = 1L;
28  
29      private JTabbedPane tabbedPane;
30  
31      private JPanel mainPanel;
32  
33      private FileListPanel fileListPanel;
34  
35      private JRadioButton scannedRadioButton;
36  
37      private JRadioButton encryptedRadioButton;
38  
39      private JRadioButton nonOutlineRadioButton;
40  
41      private JRadioButton hasAnnotationsRadioButton;
42  
43      private JSpinner thresholdSpinner;
44  
45      private JCheckBox isRecursiveSearched;
46  
47      private JPanel resultPanel;
48  
49      private JTable resultTable;
50  
51      private DefaultTableModel resultTableModel;
52  
53      private JButton searchButton;
54      private JButton cancelButton;
55  
56      private JProgressBar progressBar;
57  
58      private JMenuItem openDirMenuItem;
59  
60      private JMenuItem copyFilesMenuItem;
61  
62      private SearchThread searchThread;
63  
64      final private List<File> resultFileList = new ArrayList<>();
65  
66  
67      @Override
68      public void initUI() {
69          tabbedPane = new JTabbedPane();
70          add(tabbedPane);
71  
72          createMainPanel();
73          tabbedPane.addTab("Option", null, mainPanel, "Show Search Options");
74  
75          createResultPanel();
76          tabbedPane.addTab("Result", null, resultPanel, "Show Search Result");
77      }
78  
79      private void createMainPanel() {
80          mainPanel = new JPanel();
81          mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
82  
83          fileListPanel = new FileListPanel();
84  
85          JPanel checkOptionPanel = new JPanel();
86          checkOptionPanel.setLayout(new BoxLayout(checkOptionPanel, BoxLayout.X_AXIS));
87          checkOptionPanel.setBorder(BorderFactory.createTitledBorder("Check Options"));
88  
89          ButtonGroup buttonGroup = new ButtonGroup();
90          ItemListener itemListener = new RadioButtonItemListener();
91  
92          scannedRadioButton = new JRadioButton("查找扫描的PDF文件");
93          scannedRadioButton.setSelected(true);
94          scannedRadioButton.addItemListener(itemListener);
95          buttonGroup.add(scannedRadioButton);
96  
97          encryptedRadioButton = new JRadioButton("查找加密的PDF文件");
98          encryptedRadioButton.addItemListener(itemListener);
99          buttonGroup.add(encryptedRadioButton);
100 
101         nonOutlineRadioButton = new JRadioButton("查找没有目录的PDF文件");
102         nonOutlineRadioButton.addItemListener(itemListener);
103         buttonGroup.add(nonOutlineRadioButton);
104 
105         hasAnnotationsRadioButton = new JRadioButton("查找有注释的PDF文件");
106         hasAnnotationsRadioButton.addItemListener(itemListener);
107         buttonGroup.add(hasAnnotationsRadioButton);
108 
109         JPanel typePanel = new JPanel();
110         typePanel.setLayout(new FlowLayout(FlowLayout.LEFT,10,3));
111         typePanel.add(scannedRadioButton);
112         typePanel.add(encryptedRadioButton);
113         typePanel.add(nonOutlineRadioButton);
114         typePanel.add(hasAnnotationsRadioButton);
115 
116         JLabel thresholdLabel = new JLabel("Threshold: ");
117         thresholdSpinner = new JSpinner();
118         thresholdSpinner.setModel(new SpinnerNumberModel(1, 0, 100, 1));
119 
120         checkOptionPanel.add(typePanel);
121         checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
122         checkOptionPanel.add(thresholdLabel);
123         checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
124         checkOptionPanel.add(thresholdSpinner);
125         checkOptionPanel.add(Box.createHorizontalGlue());
126 
127         JPanel searchOptionPanel = new JPanel();
128         searchOptionPanel.setLayout(new BoxLayout(searchOptionPanel, BoxLayout.X_AXIS));
129         searchOptionPanel.setBorder(BorderFactory.createTitledBorder("Search Options"));
130 
131         isRecursiveSearched = new JCheckBox("Recursive");
132         isRecursiveSearched.setSelected(true);
133         searchOptionPanel.add(isRecursiveSearched);
134         searchOptionPanel.add(Box.createHorizontalGlue());
135 
136         JPanel operationPanel = new JPanel();
137         operationPanel.setLayout(new BoxLayout(operationPanel, BoxLayout.X_AXIS));
138         operationPanel.setBorder(BorderFactory.createTitledBorder("Operations"));
139 
140         JPanel buttonPanel = new JPanel();
141         buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
142 
143         searchButton = new JButton("Search");
144         cancelButton = new JButton("Cancel");
145         cancelButton.setEnabled(false);
146         searchButton.addActionListener(new OperationButtonActionListener());
147         cancelButton.addActionListener(new OperationButtonActionListener());
148         operationPanel.add(searchButton);
149         operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
150         operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
151         operationPanel.add(cancelButton);
152         operationPanel.add(Box.createHorizontalGlue());
153 
154         progressBar = new JProgressBar();
155         progressBar.setStringPainted(true);
156         progressBar.setString("Ready");
157 
158         mainPanel.add(fileListPanel);
159         mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
160         mainPanel.add(checkOptionPanel);
161         mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
162         mainPanel.add(searchOptionPanel);
163         mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
164         mainPanel.add(operationPanel);
165         mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
166         mainPanel.add(progressBar);
167     }
168 
169     private void createResultPanel() {
170         resultPanel = new JPanel();
171         resultPanel.setLayout(new BoxLayout(resultPanel, BoxLayout.Y_AXIS));
172 
173         resultTableModel = new PdfFilesTableModel(new Vector<>(), PdfFilesConstants.COLUMN_NAMES);
174         resultTable = new JTable(resultTableModel);
175 
176         resultTable.setDefaultRenderer(Vector.class, new PdfFilesTableCellRenderer());
177 
178         for (int i = 0; i < resultTable.getColumnCount(); i++) {
179             resultTable.getColumn(resultTable.getColumnName(i)).setCellRenderer(new PdfFilesTableCellRenderer());
180         }
181 
182         resultTable.addMouseListener(new MyMouseListener());
183 
184         resultTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
185 
186         JScrollPane scrollPane = new JScrollPane(resultTable);
187         resultPanel.add(scrollPane);
188     }
189 
190     private void processFile(File file) {
191         if (scannedRadioButton.isSelected()) {
192             int threshold = (Integer) thresholdSpinner.getValue();
193             if (PdfUtils.isScannedPdf(file, threshold)) {
194                 resultFileList.add(file);
195             }
196         } else if (encryptedRadioButton.isSelected()) {
197             if (PdfUtils.isEncryptedPdf(file)) {
198                 resultFileList.add(file);
199             }
200         } else if (nonOutlineRadioButton.isSelected()) {
201             if (PdfUtils.isNonOutlinePdf(file)) {
202                 resultFileList.add(file);
203             }
204         } else if (hasAnnotationsRadioButton.isSelected()) {
205             if (PdfUtils.hasAnnotations(file)) {
206                 resultFileList.add(file);
207             }
208         } else {
209             logger.error("Invalid option selected");
210         }
211     }
212 
213     class MyMouseListener extends MouseAdapter {
214         @Override
215         public void mouseReleased(MouseEvent e) {
216             super.mouseReleased(e);
217             int r = resultTable.rowAtPoint(e.getPoint());
218             if (r >= 0 && r < resultTable.getRowCount()) {
219                 if (!resultTable.isRowSelected(r)) {
220                     resultTable.setRowSelectionInterval(r, r);
221                 }
222             } else {
223                 resultTable.clearSelection();
224             }
225             int[] rowsIndex = resultTable.getSelectedRows();
226             if (rowsIndex == null || rowsIndex.length == 0) {
227                 return;
228             }
229             if (e.isPopupTrigger() && e.getComponent() instanceof JTable) {
230                 JPopupMenu popupmenu = new JPopupMenu();
231                 MyMenuActionListener menuActionListener = new MyMenuActionListener();
232 
233                 if (rowsIndex.length == 1) {
234                     openDirMenuItem = new JMenuItem("Open parent folder of this file");
235                     openDirMenuItem.addActionListener(menuActionListener);
236                     popupmenu.add(openDirMenuItem);
237                     popupmenu.addSeparator();
238                 }
239 
240                 copyFilesMenuItem = new JMenuItem("Copy selected files to...");
241                 copyFilesMenuItem.addActionListener(menuActionListener);
242                 popupmenu.add(copyFilesMenuItem);
243 
244                 popupmenu.show(e.getComponent(), e.getX(), e.getY());
245             }
246         }
247     }
248 
249     class MyMenuActionListener implements ActionListener {
250         @Override
251         public void actionPerformed(ActionEvent actionEvent) {
252             Object source = actionEvent.getSource();
253             if (source.equals(openDirMenuItem)) {
254                 onOpenDir();
255             } else if (source.equals(copyFilesMenuItem)) {
256                 onCopyFiles();
257             } else {
258                 logger.error("invalid source");
259             }
260         }
261 
262         private void onOpenDir() {
263             int rowIndex = resultTable.getSelectedRow();
264             String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
265             File parent = new File(parentPath);
266             RevealFileUtils.revealDirectory(parent);
267         }
268 
269         private void onCopyFiles() {
270             int[] selectedRows = resultTable.getSelectedRows();
271             if (selectedRows.length == 0) {
272                 JOptionPane.showMessageDialog(PdfFinderPanel.this, "No rows selected", "Error", JOptionPane.ERROR_MESSAGE);
273                 return;
274             }
275             List<File> filesToCopy = new ArrayList<>();
276             for (int rowIndex : selectedRows) {
277                 String filePath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_NAME).getModelIndex()).toString();
278                 String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
279                 File file = new File(parentPath, filePath);
280                 filesToCopy.add(file);
281             }
282             JFileChooser fileChooser = new JFileChooser();
283             fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
284             fileChooser.setDialogTitle("Select Target Directory");
285             int returnValue = fileChooser.showOpenDialog(PdfFinderPanel.this);
286             if (returnValue != JFileChooser.APPROVE_OPTION) {
287                 return;
288             }
289             File targetDir = fileChooser.getSelectedFile();
290             for (File file : filesToCopy) {
291                 try {
292                     org.apache.commons.io.FileUtils.copyFileToDirectory(file, targetDir);
293                 } catch (Exception e) {
294                     logger.error("Copy file failed: " + file.getAbsolutePath(), e);
295                 }
296             }
297         }
298 
299 
300     }
301 
302     class OperationButtonActionListener implements ActionListener {
303         @Override
304         public void actionPerformed(ActionEvent e) {
305             Object source = e.getSource();
306             if (source.equals(searchButton)) {
307                 searchButton.setEnabled(false);
308                 cancelButton.setEnabled(true);
309                 searchThread = new SearchThread(isRecursiveSearched.isSelected());
310                 searchThread.start();
311             } else if (source.equals(cancelButton)) {
312                 searchButton.setEnabled(true);
313                 cancelButton.setEnabled(false);
314                 if (searchThread.isAlive()) {
315                     searchThread.interrupt();
316                     searchThread.executorService.shutdownNow();
317                 }
318             }
319 
320         }
321     }
322 
323     private void showResult() {
324         SwingUtilities.invokeLater(() -> {
325             int index = 0;
326             for (File file : resultFileList) {
327                 index++;
328                 Vector<Object> rowData = getRowVector(index, file);
329                 resultTableModel.addRow(rowData);
330             }
331             tabbedPane.setSelectedIndex(1);
332         });
333     }
334 
335     private Vector<Object> getRowVector(int index, File file) {
336         Vector<Object> rowData = new Vector<>();
337         rowData.add(index);
338         rowData.add(file.getParent());
339         rowData.add(file.getName());
340         rowData.add(FileUtils.sizeOfInHumanFormat(file));
341         rowData.add(DateUtils.millisecondToHumanFormat(file.lastModified()));
342         return rowData;
343     }
344 
345     class SearchThread extends Thread {
346         public final ExecutorService executorService;
347         private final AtomicInteger processedFiles = new AtomicInteger(0);
348         private int totalFiles = 0;
349         private final boolean isRecursiveSearched;
350 
351         public SearchThread(boolean isRecursiveSearched) {
352             super();
353             this.isRecursiveSearched = isRecursiveSearched;
354             this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
355 
356             SwingUtilities.invokeLater(() -> {
357                 progressBar.setValue(0);
358                 progressBar.setString("Starting search...");
359             });
360         }
361 
362         @Override
363         public void run() {
364             try {
365                 resultFileList.clear();
366                 SwingUtilities.invokeLater(() -> resultTableModel.setRowCount(0));
367 
368                 List<File> fileList = fileListPanel.getFileList();
369                 Set<File> fileSet = new TreeSet<>();
370                 String[] extensions = new String[]{"pdf", "PDF"};
371                 for (File file : fileList) {
372                     fileSet.addAll(FileUtils.listFiles(file, extensions, isRecursiveSearched));
373                 }
374 
375                 List<Future<?>> futures = new ArrayList<>();
376                 totalFiles = fileSet.size();
377                 updateProgress();
378 
379                 for (File file : fileSet) {
380                     if (Thread.currentThread().isInterrupted()) {
381                         return;
382                     }
383                     futures.add(executorService.submit(() -> {
384                         if (Thread.currentThread().isInterrupted()) {
385                             return null;
386                         }
387                         processFile(file);
388                         incrementProcessedFiles();
389                         return null;
390                     }));
391                 }
392 
393                 // Wait for all tasks to complete
394                 for (Future<?> future : futures) {
395                     try {
396                         future.get();
397                     } catch (InterruptedException e) {
398                         logger.error("Search interrupted", e);
399                         Thread.currentThread().interrupt(); // Restore interrupted status
400                         return;
401                     }
402                 }
403 
404                 showResult();
405             } catch (Exception e) {
406                 logger.error("Search failed", e);
407                 SwingUtilities.invokeLater(() -> progressBar.setString("Search failed"));
408             } finally {
409                 executorService.shutdown();
410                 SwingUtilities.invokeLater(() -> {
411                     searchButton.setEnabled(true);
412                     cancelButton.setEnabled(false);
413                 });
414             }
415         }
416 
417         private void incrementProcessedFiles() {
418             processedFiles.incrementAndGet();
419             updateProgress();
420         }
421 
422         private void updateProgress() {
423             if (totalFiles > 0) {
424                 SwingUtilities.invokeLater(() -> {
425                     int processed = processedFiles.get();
426                     int percentage = (int) ((processed * 100.0) / totalFiles);
427                     progressBar.setValue(percentage);
428                     progressBar.setString(String.format("Processing: %d/%d files (%d%%)", processed, totalFiles, percentage));
429                 });
430             }
431         }
432     }
433 
434     class RadioButtonItemListener implements ItemListener {
435         @Override
436         public void itemStateChanged(ItemEvent e) {
437             thresholdSpinner.setEnabled(scannedRadioButton.isSelected());
438         }
439     }
440 }