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
394 for (Future<?> future : futures) {
395 try {
396 future.get();
397 } catch (InterruptedException e) {
398 logger.error("Search interrupted", e);
399 Thread.currentThread().interrupt();
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 }