Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: src/org/adblockplus/android/ProxyService.java

Issue 8484110: ABP/Android proxy service (Closed)
Patch Set: ABP/Android proxy service Created Nov. 1, 2012, 9:46 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 package org.adblockplus.android;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.lang.reflect.Method;
6 import java.net.InetAddress;
7 import java.net.ServerSocket;
8 import java.util.List;
9 import java.util.Properties;
10 import java.util.concurrent.TimeoutException;
11
12 import sunlabs.brazil.server.Server;
13 import sunlabs.brazil.util.Base64;
14 import android.app.Notification;
15 import android.app.NotificationManager;
16 import android.app.PendingIntent;
17 import android.app.Service;
18 import android.content.BroadcastReceiver;
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.IntentFilter;
22 import android.content.SharedPreferences;
23 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.net.ConnectivityManager;
26 import android.net.NetworkInfo;
27 import android.os.Binder;
28 import android.os.Build;
29 import android.os.Handler;
30 import android.os.IBinder;
31 import android.preference.PreferenceManager;
32 import android.util.Log;
33 import android.widget.Toast;
34
35 import com.stericson.RootTools.RootTools;
36 import com.stericson.RootTools.RootToolsException;
37
38 public class ProxyService extends Service implements OnSharedPreferenceChangeLis tener
39 {
40 static
41 {
42 RootTools.debugMode = false;
43 }
44
45 private static final String TAG = "ProxyService";
46
47 private final static int DEFAULT_TIMEOUT = 3000;
48 private final static int NO_TRAFFIC_TIMEOUT = 5 * 60 * 1000; // 5 minutes
49
50 final static int ONGOING_NOTIFICATION_ID = R.string.app_name;
51 private final static int NOTRAFFIC_NOTIFICATION_ID = R.string.app_name + 3;
52
53 /**
54 * Broadcasted when service starts or stops.
55 */
56 public final static String BROADCAST_STATE_CHANGED = "org.adblockplus.android. service.state";
57 /**
58 * Broadcasted if proxy fails to start.
59 */
60 public final static String BROADCAST_PROXY_FAILED = "org.adblockplus.android.p roxy.failure";
61
62 private final static String IPTABLES_RETURN = " -t nat -m owner --uid-owner {{ UID}} -A OUTPUT -p tcp -j RETURN\n";
63 private final static String IPTABLES_ADD_HTTP = " -t nat -A OUTPUT -p tcp --dp ort 80 -j REDIRECT --to {{PORT}}\n";
64
65 private Notification ongoingNotification;
66 private PendingIntent contentIntent;
67
68 private Handler notrafficHandler;
69
70 protected ProxyServer proxy = null;
71 protected int port;
72
73 /**
74 * Indicates that service is working with root privileges.
75 */
76 private boolean isTransparent = false;
77 /**
78 * Indicates that service has autoconfigured Android proxy settings (version
79 * 3.1+).
80 */
81 private boolean isNativeProxy = false;
82
83 private String iptables = null;
84
85 @Override
86 public void onCreate()
87 {
88 super.onCreate();
89
90 // Get port for local proxy
91 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this );
92 String p = prefs.getString(getString(R.string.pref_port), null);
93 try
94 {
95 port = p != null ? Integer.valueOf(p) : getResources().getInteger(R.intege r.def_port);
96 }
97 catch (NumberFormatException e)
98 {
99 Toast.makeText(this, getString(R.string.msg_badport) + ": " + p, Toast.LEN GTH_LONG).show();
100 port = getResources().getInteger(R.integer.def_port);
101 }
102
103 // Try to read user proxy settings
104 String proxyHost = null;
105 String proxyPort = null;
106 String proxyExcl = null;
107 String proxyUser = null;
108 String proxyPass = null;
109
110 if (Build.VERSION.SDK_INT >= 12) // Honeycomb 3.1
Andrey Novikov 2012/11/09 09:23:47 What exactly are you referencing?
Felix Dahlke 2012/11/09 14:40:46 I think it was AdvancedPreferences.hasNativeProxy.
111 {
112 // Read system settings
113 proxyHost = System.getProperty("http.proxyHost");
114 proxyPort = System.getProperty("http.proxyPort");
115 proxyExcl = System.getProperty("http.nonProxyHosts");
116
117 Log.d(TAG, "PRX: " + proxyHost + ":" + proxyPort + "(" + proxyExcl + ")");
118 String[] px = ProxySettings.getUserProxy(getApplicationContext()); // not used but left for future reference
Felix Dahlke 2012/11/08 16:37:50 Future reference? It is in fact used below, if onl
Felix Dahlke 2012/11/09 14:40:46 I see, I misunderstood that then. I thought you me
119 if (px != null)
120 Log.d(TAG, "PRX: " + px[0] + ":" + px[1] + "(" + px[2] + ")");
121 }
122 else
123 {
124 // Read application settings
125 proxyHost = prefs.getString(getString(R.string.pref_proxyhost), null);
126 proxyPort = prefs.getString(getString(R.string.pref_proxyport), null);
127 proxyUser = prefs.getString(getString(R.string.pref_proxyuser), null);
128 proxyPass = prefs.getString(getString(R.string.pref_proxypass), null);
129 }
130
131 // Check for root privileges and try to install transparent proxy
132 if (RootTools.isAccessGiven())
133 {
134 try
135 {
136 iptables = getIptables();
Andrey Novikov 2012/11/09 09:23:47 Done.
137 if (iptables != null)
138 {
139 StringBuffer cmd = new StringBuffer();
140 int uid = getPackageManager().getPackageInfo(getPackageName(), 0).appl icationInfo.uid;
141 cmd.append(iptables);
142 cmd.append(IPTABLES_RETURN.replace("{{UID}}", String.valueOf(uid)));
143 cmd.append(iptables);
144 cmd.append(IPTABLES_ADD_HTTP.replace("{{PORT}}", String.valueOf(port)) );
145 String rules = cmd.toString();
146 RootTools.sendShell(rules, DEFAULT_TIMEOUT);
147 isTransparent = true;
148 }
149 }
150 catch (NameNotFoundException e)
151 {
152 e.printStackTrace();
Felix Dahlke 2012/11/08 16:37:50 Here and below, I'd prefer Log.e().
Andrey Novikov 2012/11/09 09:23:47 Done.
153 }
154 catch (IOException e)
155 {
156 e.printStackTrace();
157 }
158 catch (RootToolsException e)
159 {
160 e.printStackTrace();
161 }
162 catch (TimeoutException e)
163 {
164 e.printStackTrace();
165 }
166 }
167
168 if (!isTransparent)
169 {
170 // Try to set native proxy
171 isNativeProxy = ProxySettings.setConnectionProxy(getApplicationContext(), "127.0.0.1", port, "");
172
173 if (isNativeProxy)
174 {
175 registerReceiver(connectionReceiver, new IntentFilter(ConnectivityManage r.CONNECTIVITY_ACTION));
176 registerReceiver(connectionReceiver, new IntentFilter("android.net.wifi. LINK_CONFIGURATION_CHANGED"));
177 }
178 }
179
180 // Start engine
181 AdblockPlus.getApplication().startEngine();
182
183 registerReceiver(proxyReceiver, new IntentFilter(ProxyService.BROADCAST_PROX Y_FAILED));
184 registerReceiver(matchesReceiver, new IntentFilter(AdblockPlus.BROADCAST_FIL TER_MATCHES));
185
186 // Start proxy
187 if (proxy == null)
188 {
189 ServerSocket listen = null;
190 try
191 {
192 // TODO Add port travel
193 listen = new ServerSocket(port, 1024);
194 }
195 catch (IOException e)
196 {
197 sendBroadcast(new Intent(BROADCAST_PROXY_FAILED).putExtra("msg", e.getMe ssage()));
198 Log.e(TAG, null, e);
199 return;
200 }
201
202 Properties config = new Properties();
203 config.put("handler", "main");
204 config.put("main.prefix", "");
205 config.put("main.class", "sunlabs.brazil.server.ChainHandler");
206 if (isTransparent)
207 {
208 config.put("main.handlers", "urlmodifier adblock");
209 config.put("urlmodifier.class", "org.adblockplus.brazil.TransparentProxy Handler");
210 }
211 else
212 {
213 config.put("main.handlers", "https adblock");
214 config.put("https.class", "org.adblockplus.brazil.SSLConnectionHandler") ;
215 }
216 config.put("adblock.class", "org.adblockplus.brazil.RequestHandler");
217 // config.put("adblock.proxylog", "yes");
218
219 configureUserProxy(config, proxyHost, proxyPort, proxyExcl, proxyUser, pro xyPass);
220
221 proxy = new ProxyServer();
222 proxy.logLevel = Server.LOG_DIAGNOSTIC;
223 proxy.setup(listen, config.getProperty("handler"), config);
224 proxy.start();
225 }
226
227 prefs.registerOnSharedPreferenceChangeListener(this);
228
229 String msg = getString(isTransparent ? R.string.notif_all : isNativeProxy ? R.string.notif_wifi : R.string.notif_waiting);
230 if (!isTransparent && !isNativeProxy)
231 {
232 // Initiate no traffic check
233 notrafficHandler = new Handler();
234 notrafficHandler.postDelayed(noTraffic, NO_TRAFFIC_TIMEOUT);
235 }
236 // Lock service
237 ongoingNotification = new Notification();
238 ongoingNotification.when = 0;
239 contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, Preferen ces.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TA SK), 0);
240 ongoingNotification.icon = R.drawable.ic_stat_blocking;
241 ongoingNotification.setLatestEventInfo(getApplicationContext(), getText(R.st ring.app_name), msg, contentIntent);
242 startForeground(ONGOING_NOTIFICATION_ID, ongoingNotification);
243
244 sendBroadcast(new Intent(BROADCAST_STATE_CHANGED).putExtra("enabled", true). putExtra("port", port).putExtra("manual", !isTransparent && !isNativeProxy));
245 Log.i(TAG, "Service started");
246 }
247
248 @Override
249 public void onDestroy()
250 {
251 super.onDestroy();
252
253 stopNoTrafficCheck(false);
254
255 unregisterReceiver(matchesReceiver);
256 unregisterReceiver(proxyReceiver);
257
258 // Stop IP redirecting
259 if (isTransparent)
260 {
261 new Thread() {
262 @Override
263 public void run()
264 {
265 try
266 {
267 RootTools.sendShell(iptables + " -t nat -F OUTPUT", DEFAULT_TIMEOUT) ;
268 }
269 catch (Exception e)
270 {
271 e.printStackTrace();
Felix Dahlke 2012/11/08 16:37:50 Log.e()?
Andrey Novikov 2012/11/09 09:23:47 Done.
272 }
273 }
274 }.start();
275 }
276
277 // Clear native proxy
278 if (isNativeProxy)
279 {
280 unregisterReceiver(connectionReceiver);
281 clearConnectionProxy();
282 }
283
284 sendBroadcast(new Intent(BROADCAST_STATE_CHANGED).putExtra("enabled", false) );
285
286 // Stop proxy server
287 proxy.close();
288
289 // Stop engine if not in interactive mode
290 AdblockPlus.getApplication().stopEngine(false);
291
292 // Release service lock
293 stopForeground(true);
294
295 Log.i(TAG, "Service stopped");
296 }
297
298 /**
299 * Restores system proxy settings via native call on Android 3.1+ devices usin g
300 * Java reflection.
301 */
302 private void clearConnectionProxy()
303 {
304 String proxyHost = (String) proxy.props.getProperty("adblock.proxyHost");
305 String proxyPort = (String) proxy.props.getProperty("adblock.proxyPort");
306 String proxyExcl = (String) proxy.props.getProperty("adblock.proxyExcl");
307 int port = 0;
308 try
309 {
310 if (proxyHost != null)
311 port = Integer.valueOf(proxyPort);
312 }
313 catch (NumberFormatException e)
314 {
315 Log.e(TAG, "Bad port setting", e);
316 }
317 ProxySettings.setConnectionProxy(getApplicationContext(), proxyHost, port, p roxyExcl);
318 }
319
320 /**
321 * Sets user proxy settings in proxy service properties.
322 */
323 private void configureUserProxy(Properties config, String proxyHost, String pr oxyPort, String proxyExcl, String proxyUser, String proxyPass)
324 {
325 // Clean previous settings
326 config.remove("adblock.proxyHost");
327 config.remove("adblock.proxyPort");
328 config.remove("adblock.auth");
329 config.remove("adblock.proxyExcl");
330 if (!isTransparent)
331 {
332 config.remove("https.proxyHost");
333 config.remove("https.proxyPort");
334 config.remove("https.auth");
335 }
336
337 if (isNativeProxy)
338 passProxySettings(proxyHost, proxyPort, proxyExcl);
339
340 // Check if there are any settings
341 if (proxyHost == null || "".equals(proxyHost))
342 return;
343
344 // Check for dirty proxy settings - this indicated previous crash:
345 // proxy points to ourselves
346 // proxy port is null, 0 or not a number
347 // proxy is 127.0.0.1:8080
348 if (proxyPort == null)
349 return;
350 int p = 0;
351 try
352 {
353 p = Integer.valueOf(proxyPort);
354 }
355 catch (NumberFormatException e)
356 {
357 return;
358 }
359 if (p == 0 || isLocalHost(proxyHost) && (p == port || p == 8080))
360 {
361 if (isNativeProxy)
362 passProxySettings(null, null, null);
363 return;
364 }
365
366 config.put("adblock.proxyHost", proxyHost);
367 config.put("adblock.proxyPort", proxyPort);
368 if (!isTransparent)
369 {
370 config.put("https.proxyHost", proxyHost);
371 config.put("https.proxyPort", proxyPort);
372 }
373
374 // TODO Not implemented in our proxy but needed to restore settings
375 if (proxyExcl != null)
376 config.put("adblock.proxyExcl", proxyExcl);
377
378 if (proxyUser != null && !"".equals(proxyUser) && proxyPass != null && !"".e quals(proxyPass))
379 {
380 // Base64 encode user:password
381 String proxyAuth = "Basic " + new String(Base64.encode(proxyUser + ":" + p roxyPass));
382 config.put("adblock.auth", proxyAuth);
383 if (!isTransparent)
384 config.put("https.auth", proxyAuth);
385 }
386 }
387
388 private void passProxySettings(String proxyHost, String proxyPort, String prox yExcl)
389 {
390 try
391 {
392 CrashHandler handler = (CrashHandler) Thread.getDefaultUncaughtExceptionHa ndler();
393 handler.saveProxySettings(proxyHost, proxyPort, proxyExcl);
394 }
395 catch (ClassCastException e)
396 {
397 // ignore - default handler in use
398 }
399 }
400
401 @Override
402 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Str ing key)
403 {
404 if (Build.VERSION.SDK_INT < 12) // Honeycomb 3.1
Felix Dahlke 2012/11/08 16:37:50 Like before, I think there's a global variable or
Felix Dahlke 2012/11/09 14:40:46 See my reasoning above. If we're doing this check
405 {
406 if (key.equals(getString(R.string.pref_proxyhost)) || key.equals(getString (R.string.pref_proxyport)) || key.equals(getString(R.string.pref_proxyuser))
407 || key.equals(getString(R.string.pref_proxypass)))
408 {
409 String proxyHost = sharedPreferences.getString(getString(R.string.pref_p roxyhost), null);
410 String proxyPort = sharedPreferences.getString(getString(R.string.pref_p roxyport), null);
411 String proxyUser = sharedPreferences.getString(getString(R.string.pref_p roxyuser), null);
412 String proxyPass = sharedPreferences.getString(getString(R.string.pref_p roxypass), null);
413 if (proxy != null)
414 {
415 configureUserProxy(proxy.props, proxyHost, proxyPort, null, proxyUser, proxyPass);
416 proxy.restart(proxy.props.getProperty("handler"));
417 }
418 }
419 }
420 }
421
422 public boolean isTransparent()
423 {
424 return isTransparent;
Felix Dahlke 2012/11/08 16:37:50 I believe the bean conventions says that boolean g
Andrey Novikov 2012/11/09 09:23:47 Done.
425 }
426
427 public boolean isNativeProxy()
428 {
429 return isNativeProxy;
430 }
431
432 /**
433 * Checks if specified host is local.
434 */
435 private static final boolean isLocalHost(String host)
436 {
437 if (host == null)
438 return false;
439
440 try
441 {
442 if (host != null)
443 {
444 if (host.equalsIgnoreCase("localhost"))
445 return true;
446
447 String className = "android.net.NetworkUtils";
448 Class<?> c = Class.forName(className);
449 /*
450 * InetAddress address = NetworkUtils.numericToInetAddress(host);
451 */
452 Method method = c.getMethod("numericToInetAddress", String.class);
453 InetAddress address = (InetAddress) method.invoke(null, host);
454
455 if (address.isLoopbackAddress())
456 return true;
457 }
458 }
459 catch (Exception e)
460 {
Felix Dahlke 2012/11/08 16:37:50 Comment or logging please.
Andrey Novikov 2012/11/09 09:23:47 Done.
461 }
462 return false;
463 }
464
465 /**
466 * Returns path to iptables executable.
467 */
468 public String getIptables() throws IOException, RootToolsException, TimeoutExc eption
Felix Dahlke 2012/11/08 16:37:50 Bean convention again: Can you think of another pr
Andrey Novikov 2012/11/12 08:53:12 Done.
469 {
470 if (!RootTools.isAccessGiven())
471 return null;
472
473 File ipt = getFileStreamPath("iptables");
474
475 if (!ipt.exists())
476 return null;
Felix Dahlke 2012/11/08 16:37:50 I think this is worth some logging, would help deb
Andrey Novikov 2012/11/09 09:23:47 Done.
477
478 String path = ipt.getAbsolutePath();
479
480 RootTools.sendShell("chmod 700 " + path, DEFAULT_TIMEOUT);
Andrey Novikov 2012/11/09 09:23:47 We do not want any others to be able to execute it
Felix Dahlke 2012/11/09 14:40:46 See above, I thought it was using a system wide ex
481
482 boolean compatible = false;
483 boolean version = false;
484
485 String command = path + " --version\n" + path + " -L -t nat -n\n";
486
487 List<String> result = RootTools.sendShell(command, DEFAULT_TIMEOUT);
488 for (String line : result)
489 {
490 if (line.contains("OUTPUT"))
491 compatible = true;
492 if (line.contains("v1.4."))
493 version = true;
494 }
495
496 if (!compatible || !version)
497 return null;
498
499 return path;
500 }
501
502 public List<String> getIptablesOutput()
503 {
504 if (iptables == null)
505 return null;
506
507 String command = iptables + " -L -t nat -n\n";
508 try
509 {
510 return RootTools.sendShell(command, DEFAULT_TIMEOUT);
511 }
512 catch (Exception e)
513 {
514 Log.e(TAG, "Failed to get iptables configuration", e);
515 return null;
516 }
517 }
518
519 /**
520 * Stops no traffic check, optionally resetting notification message.
521 *
522 * @param changeStatus
523 * true if notification message should me set to normal operating
524 * mode
525 */
526 private void stopNoTrafficCheck(boolean changeStatus)
527 {
528 if (notrafficHandler != null)
529 {
530 notrafficHandler.removeCallbacks(noTraffic);
531 if (changeStatus)
532 {
533 NotificationManager notificationManager = (NotificationManager) getSyste mService(NOTIFICATION_SERVICE);
534 ongoingNotification.setLatestEventInfo(ProxyService.this, getText(R.stri ng.app_name), getText(R.string.notif_wifi), contentIntent);
535 notificationManager.notify(ONGOING_NOTIFICATION_ID, ongoingNotification) ;
536 }
537 }
538 notrafficHandler = null;
539 }
540
541 private final IBinder binder = new LocalBinder();
542
543 public final class LocalBinder extends Binder
544 {
545 public ProxyService getService()
546 {
547 return ProxyService.this;
548 }
549 }
550
551 @Override
552 public IBinder onBind(Intent intent)
553 {
554 return binder;
555 }
556
557 /**
558 * Executed if no traffic is detected after a period of time. Notifies user
559 * about possible configuration problems.
560 */
561 private Runnable noTraffic = new Runnable() {
562 public void run()
563 {
564 // Show warning notification
565 Notification notification = new Notification();
566 notification.icon = R.drawable.ic_stat_warning;
567 notification.when = System.currentTimeMillis();
568 notification.flags |= Notification.FLAG_AUTO_CANCEL;
569 notification.defaults |= Notification.DEFAULT_SOUND;
570 Intent intent = new Intent(ProxyService.this, ConfigurationActivity.class) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
571 intent.putExtra("port", port);
572 PendingIntent contentIntent = PendingIntent.getActivity(ProxyService.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
573 notification.setLatestEventInfo(ProxyService.this, getText(R.string.app_na me), getString(R.string.notif_notraffic), contentIntent);
574 NotificationManager notificationManager = (NotificationManager) getSystemS ervice(NOTIFICATION_SERVICE);
575 notificationManager.notify(NOTRAFFIC_NOTIFICATION_ID, notification);
576 }
577 };
578
579 /**
580 * Stops no traffic check if traffic is detected by proxy service.
581 */
582 private BroadcastReceiver matchesReceiver = new BroadcastReceiver() {
583 @Override
584 public void onReceive(final Context context, Intent intent)
585 {
586 if (intent.getAction().equals(AdblockPlus.BROADCAST_FILTER_MATCHES))
587 stopNoTrafficCheck(true);
588 }
589 };
590
591 /**
592 * Stops service if proxy fails.
593 */
594 private BroadcastReceiver proxyReceiver = new BroadcastReceiver() {
595 @Override
596 public void onReceive(final Context context, Intent intent)
597 {
598 if (intent.getAction().equals(ProxyService.BROADCAST_PROXY_FAILED))
599 {
600 stopSelf();
601 }
602 }
603 };
604
605 /**
606 * Monitors system network connection settings changes and updates proxy
607 * settings accordingly.
608 */
609 private BroadcastReceiver connectionReceiver = new BroadcastReceiver() {
610 @Override
611 public void onReceive(Context ctx, Intent intent)
612 {
613 String action = intent.getAction();
614 Log.i(TAG, "Action: " + action);
615 if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action))
616 {
617 NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_N ETWORK_INFO);
618 String typeName = info.getTypeName();
619 String subtypeName = info.getSubtypeName();
620 boolean available = info.isAvailable();
621 Log.i(TAG, "Network Type: " + typeName + ", subtype: " + subtypeName + " , available: " + available);
622 if (info.getType() == ConnectivityManager.TYPE_WIFI)
623 ProxySettings.setConnectionProxy(getApplicationContext(), "127.0.0.1", port, "");
624 }
625 else if ("android.net.wifi.LINK_CONFIGURATION_CHANGED".equals(action))
626 {
627 Object lp = intent.getParcelableExtra("linkProperties");
628 Method method;
629 try
630 {
631 /*
632 * lp.getHttpProxy();
633 */
634 method = lp.getClass().getMethod("getHttpProxy");
635 Object pp = method.invoke(lp);
636
637 String[] userProxy = ProxySettings.getUserProxy(pp);
638 if (userProxy != null && Integer.valueOf(userProxy[1]) != port)
639 {
640 Log.i(TAG, "User has set new proxy: " + userProxy[0] + ":" + userPro xy[1] + "(" + userProxy[2] + ")");
641 if (proxy != null)
642 {
643 configureUserProxy(proxy.props, userProxy[0], userProxy[1], userPr oxy[2], null, null);
644 proxy.restart(proxy.props.getProperty("handler"));
645 }
646 }
647 }
648 catch (Exception e)
649 {
650 // This should not happen
651 e.printStackTrace();
652 }
653
654 }
655 }
656 };
657
658 final class ProxyServer extends Server
659 {
660 @Override
661 public void close()
662 {
663 try
664 {
665 listen.close();
666 this.interrupt();
667 this.join();
668 }
669 catch (Exception e)
670 {
Felix Dahlke 2012/11/08 16:37:50 Logging or a comment?
671 }
672 log(LOG_WARNING, null, "server stopped");
673 }
674
675 @Override
676 public void log(int level, Object obj, String message)
677 {
678 if (level <= logLevel)
679 {
680 Log.println(7 - level, obj != null ? obj.toString() : TAG, message);
681 }
682 }
683 }
684 }
OLDNEW

Powered by Google App Engine
This is Rietveld