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