]>
Commit | Line | Data |
---|---|---|
1 | /* | |
2 | * Copyright (C) 2012-2018 Tobias Brunner | |
3 | * | |
4 | * Copyright (C) secunet Security Networks AG | |
5 | * | |
6 | * This program is free software; you can redistribute it and/or modify it | |
7 | * under the terms of the GNU General Public License as published by the | |
8 | * Free Software Foundation; either version 2 of the License, or (at your | |
9 | * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. | |
10 | * | |
11 | * This program is distributed in the hope that it will be useful, but | |
12 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | |
13 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
14 | * for more details. | |
15 | */ | |
16 | ||
17 | package org.strongswan.android.ui; | |
18 | ||
19 | import android.content.Context; | |
20 | import android.os.Build; | |
21 | import android.os.Bundle; | |
22 | import android.os.FileObserver; | |
23 | import android.os.Handler; | |
24 | import android.os.Looper; | |
25 | import android.view.LayoutInflater; | |
26 | import android.view.View; | |
27 | import android.view.ViewGroup; | |
28 | import android.widget.ArrayAdapter; | |
29 | import android.widget.ListView; | |
30 | ||
31 | import org.strongswan.android.R; | |
32 | import org.strongswan.android.logic.CharonVpnService; | |
33 | ||
34 | import java.io.BufferedReader; | |
35 | import java.io.File; | |
36 | import java.io.FileNotFoundException; | |
37 | import java.io.FileReader; | |
38 | import java.io.StringReader; | |
39 | import java.util.ArrayList; | |
40 | ||
41 | import androidx.annotation.NonNull; | |
42 | import androidx.annotation.RequiresApi; | |
43 | import androidx.fragment.app.Fragment; | |
44 | ||
45 | public class LogFragment extends Fragment | |
46 | { | |
47 | private static String SCROLL_POSITION = "SCROLL_POSITION"; | |
48 | private String mLogFilePath; | |
49 | private Handler mLogHandler; | |
50 | private ListView mLog; | |
51 | private LogAdapter mLogAdapter; | |
52 | private FileObserver mDirectoryObserver; | |
53 | private int mScrollPosition; | |
54 | ||
55 | @Override | |
56 | public void onCreate(Bundle savedInstanceState) | |
57 | { | |
58 | super.onCreate(savedInstanceState); | |
59 | ||
60 | mLogFilePath = getActivity().getFilesDir() + File.separator + CharonVpnService.LOG_FILE; | |
61 | ||
62 | mLogHandler = new Handler(Looper.getMainLooper()); | |
63 | ||
64 | File logdir = getActivity().getFilesDir(); | |
65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) | |
66 | { | |
67 | mDirectoryObserver = new LogDirectoryObserver(logdir); | |
68 | } | |
69 | else | |
70 | { | |
71 | mDirectoryObserver = new LogDirectoryObserver(logdir.getAbsolutePath()); | |
72 | } | |
73 | } | |
74 | ||
75 | @Override | |
76 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) | |
77 | { | |
78 | View view = inflater.inflate(R.layout.log_fragment, null); | |
79 | ||
80 | mLogAdapter = new LogAdapter(getActivity()); | |
81 | mLog = view.findViewById(R.id.log); | |
82 | mLog.setAdapter(mLogAdapter); | |
83 | ||
84 | mScrollPosition = -1; | |
85 | if (savedInstanceState != null) | |
86 | { | |
87 | mScrollPosition = savedInstanceState.getInt(SCROLL_POSITION, mScrollPosition); | |
88 | } | |
89 | return view; | |
90 | } | |
91 | ||
92 | @Override | |
93 | public void onSaveInstanceState(Bundle outState) | |
94 | { | |
95 | super.onSaveInstanceState(outState); | |
96 | ||
97 | if (mLog.getLastVisiblePosition() == (mLogAdapter.getCount() - 1)) | |
98 | { | |
99 | outState.putInt(SCROLL_POSITION, -1); | |
100 | } | |
101 | else | |
102 | { | |
103 | outState.putInt(SCROLL_POSITION, mLog.getFirstVisiblePosition()); | |
104 | } | |
105 | } | |
106 | ||
107 | @Override | |
108 | public void onStart() | |
109 | { | |
110 | super.onStart(); | |
111 | mLogAdapter.restart(); | |
112 | mDirectoryObserver.startWatching(); | |
113 | } | |
114 | ||
115 | @Override | |
116 | public void onStop() | |
117 | { | |
118 | super.onStop(); | |
119 | mDirectoryObserver.stopWatching(); | |
120 | mLogAdapter.stop(); | |
121 | } | |
122 | ||
123 | private class LogAdapter extends ArrayAdapter<String> implements Runnable | |
124 | { | |
125 | private BufferedReader mReader; | |
126 | private Thread mThread; | |
127 | private volatile boolean mRunning; | |
128 | ||
129 | public LogAdapter(@NonNull Context context) | |
130 | { | |
131 | super(context, R.layout.log_list_item, R.id.log_line); | |
132 | } | |
133 | ||
134 | public void restart() | |
135 | { | |
136 | if (mRunning) | |
137 | { | |
138 | stop(); | |
139 | } | |
140 | ||
141 | clear(); | |
142 | ||
143 | try | |
144 | { | |
145 | mReader = new BufferedReader(new FileReader(mLogFilePath)); | |
146 | } | |
147 | catch (FileNotFoundException e) | |
148 | { | |
149 | mReader = new BufferedReader(new StringReader("")); | |
150 | } | |
151 | mRunning = true; | |
152 | mThread = new Thread(this); | |
153 | mThread.start(); | |
154 | } | |
155 | ||
156 | public void stop() | |
157 | { | |
158 | try | |
159 | { | |
160 | mRunning = false; | |
161 | mThread.interrupt(); | |
162 | mThread.join(); | |
163 | } | |
164 | catch (InterruptedException e) | |
165 | { | |
166 | } | |
167 | } | |
168 | ||
169 | private void logLines(final ArrayList<String> lines) | |
170 | { | |
171 | mLogHandler.post(() -> { | |
172 | boolean scroll = getCount() == 0; | |
173 | setNotifyOnChange(false); | |
174 | for (String line : lines) | |
175 | { | |
176 | if (getResources().getConfiguration().screenWidthDp < 600) | |
177 | { /* strip off prefix (month=3, day=2, time=8, thread=2, spaces=3) */ | |
178 | line = line.length() > 18 ? line.substring(18) : line; | |
179 | } | |
180 | add(line); | |
181 | } | |
182 | notifyDataSetChanged(); | |
183 | if (scroll) | |
184 | { /* scroll to the bottom or saved position after adding the first batch */ | |
185 | mLogHandler.post(() -> mLog.setSelection(mScrollPosition == -1 ? getCount() - 1 : mScrollPosition)); | |
186 | } | |
187 | }); | |
188 | } | |
189 | ||
190 | @Override | |
191 | public void run() | |
192 | { | |
193 | ArrayList<String> lines = null; | |
194 | ||
195 | while (mRunning) | |
196 | { | |
197 | try | |
198 | { /* this works as long as the file is not truncated */ | |
199 | String line = mReader.readLine(); | |
200 | if (line == null) | |
201 | { | |
202 | if (lines != null) | |
203 | { | |
204 | logLines(lines); | |
205 | lines = null; | |
206 | } | |
207 | /* wait until there is more to log */ | |
208 | Thread.sleep(1000); | |
209 | } | |
210 | else | |
211 | { | |
212 | if (lines == null) | |
213 | { | |
214 | lines = new ArrayList<>(); | |
215 | } | |
216 | lines.add(line); | |
217 | } | |
218 | } | |
219 | catch (Exception e) | |
220 | { | |
221 | break; | |
222 | } | |
223 | } | |
224 | if (lines != null) | |
225 | { | |
226 | logLines(lines); | |
227 | } | |
228 | } | |
229 | } | |
230 | ||
231 | /** | |
232 | * FileObserver that checks for changes regarding the log file. Since charon | |
233 | * truncates it (for which there is no explicit event) we check for any modification | |
234 | * to the file, keep track of the file size and reopen it if it got smaller. | |
235 | */ | |
236 | private class LogDirectoryObserver extends FileObserver | |
237 | { | |
238 | private static final int mMask = FileObserver.CREATE | FileObserver.MODIFY | FileObserver.DELETE; | |
239 | private final File mFile = new File(mLogFilePath); | |
240 | private long mSize = mFile.length(); | |
241 | ||
242 | @SuppressWarnings("deprecation") | |
243 | public LogDirectoryObserver(String path) | |
244 | { | |
245 | super(path, mMask); | |
246 | } | |
247 | ||
248 | @RequiresApi(api = Build.VERSION_CODES.Q) | |
249 | public LogDirectoryObserver(File path) | |
250 | { | |
251 | super(path, mMask); | |
252 | } | |
253 | ||
254 | @Override | |
255 | public void onEvent(int event, String path) | |
256 | { | |
257 | if (path == null || !path.equals(CharonVpnService.LOG_FILE)) | |
258 | { | |
259 | return; | |
260 | } | |
261 | switch (event) | |
262 | { /* even though we only subscribed for these we check them, | |
263 | * as strange events are sometimes received */ | |
264 | case FileObserver.CREATE: | |
265 | case FileObserver.DELETE: | |
266 | restartLogReader(); | |
267 | break; | |
268 | case FileObserver.MODIFY: | |
269 | /* if the size got smaller reopen the log file, as it was probably truncated */ | |
270 | long size = mFile.length(); | |
271 | if (size < mSize) | |
272 | { | |
273 | restartLogReader(); | |
274 | } | |
275 | mSize = size; | |
276 | break; | |
277 | } | |
278 | } | |
279 | ||
280 | private void restartLogReader() | |
281 | { | |
282 | /* we are called from a separate thread, so we use the handler */ | |
283 | mLogHandler.post(new Runnable() { | |
284 | @Override | |
285 | public void run() | |
286 | { | |
287 | mLogAdapter.restart(); | |
288 | } | |
289 | }); | |
290 | } | |
291 | } | |
292 | } |