Java tutorial
/* * This file is part of Adblock Plus <http://adblockplus.org/>, * Copyright (C) 2006-2013 Eyeo GmbH * * Adblock Plus is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * Adblock Plus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. */ package org.adblockplus.android; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Calendar; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.adblockplus.android.updater.AlarmReceiver; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.json.JSONException; import org.json.JSONObject; import org.xml.sax.SAXException; import android.app.ActivityManager; import android.app.ActivityManager.RunningServiceInfo; import android.app.AlarmManager; import android.app.Application; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.Settings; import android.util.Log; import android.widget.Toast; public class AdblockPlus extends Application { private final static String TAG = "Application"; private final static int MSG_TOAST = 1; /** * Broadcasted when filtering is enabled or disabled. */ public static final String BROADCAST_FILTERING_CHANGE = "org.adblockplus.android.filtering.status"; /** * Broadcasted when subscription status changes. */ public final static String BROADCAST_SUBSCRIPTION_STATUS = "org.adblockplus.android.subscription.status"; /** * Broadcasted when filter match check is performed. */ public final static String BROADCAST_FILTER_MATCHES = "org.adblockplus.android.filter.matches"; private List<Subscription> subscriptions; private JSThread js; /** * Indicates interactive mode (used to listen for subscription status * changes). */ private boolean interactive = false; /** * Indicates whether filtering is enabled or not. */ private boolean filteringEnabled = false; private static AdblockPlus instance; /** * Returns pointer to itself (singleton pattern). */ public static AdblockPlus getApplication() { return instance; } public int getBuildNumber() { int buildNumber = -1; try { PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0); buildNumber = pi.versionCode; } catch (NameNotFoundException e) { // ignore - this shouldn't happen Log.e(TAG, e.getMessage(), e); } return buildNumber; } /** * Opens Android application settings */ public static void showAppDetails(Context context) { String packageName = context.getPackageName(); Intent intent = new Intent(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { // above 2.3 intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", packageName, null); intent.setData(uri); } else { // below 2.3 final String appPkgName = (Build.VERSION.SDK_INT == Build.VERSION_CODES.FROYO ? "pkg" : "com.android.settings.ApplicationPkgName"); intent.setAction(Intent.ACTION_VIEW); intent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails"); intent.putExtra(appPkgName, packageName); } intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } /** * Returns device name in user-friendly format */ public static String getDeviceName() { String manufacturer = Build.MANUFACTURER; String model = Build.MODEL; if (model.startsWith(manufacturer)) return capitalize(model); else return capitalize(manufacturer) + " " + model; } public static void appendRawTextFile(Context context, StringBuilder text, int id) { InputStream inputStream = context.getResources().openRawResource(id); InputStreamReader in = new InputStreamReader(inputStream); BufferedReader buf = new BufferedReader(in); String line; try { while ((line = buf.readLine()) != null) text.append(line); } catch (IOException e) { e.printStackTrace(); } } private static String capitalize(String s) { if (s == null || s.length() == 0) return ""; char first = s.charAt(0); if (Character.isUpperCase(first)) return s; else return Character.toUpperCase(first) + s.substring(1); } /** * Checks if device has a WiFi connection available. */ public static boolean isWiFiConnected(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = null; if (connectivityManager != null) { networkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); } return networkInfo == null ? false : networkInfo.isConnected(); } /** * Checks if ProxyService is running. * * @return true if service is running */ public boolean isServiceRunning() { ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); // Actually it returns not only running services, so extra check is required for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { if (service.service.getClassName().equals(ProxyService.class.getCanonicalName()) && service.pid > 0) return true; } return false; } /** * Checks if application can write to external storage. */ public boolean checkWriteExternalPermission() { String permission = "android.permission.WRITE_EXTERNAL_STORAGE"; int res = checkCallingOrSelfPermission(permission); return res == PackageManager.PERMISSION_GRANTED; } /** * Returns list of known subscriptions. */ public List<Subscription> getSubscriptions() { if (subscriptions == null) { subscriptions = new ArrayList<Subscription>(); SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser parser; try { parser = factory.newSAXParser(); parser.parse(getAssets().open("subscriptions.xml"), new SubscriptionParser(subscriptions)); } catch (ParserConfigurationException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } catch (SAXException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } catch (IOException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } } return subscriptions; } /** * Returns subscription information. * * @param url * subscription url */ public Subscription getSubscription(String url) { List<Subscription> subscriptions = getSubscriptions(); for (Subscription subscription : subscriptions) { if (subscription.url.equals(url)) return subscription; } return null; } /** * Adds provided subscription and removes previous subscriptions if any. * * @param subscription * Subscription to add */ public void setSubscription(Subscription subscription) { if (subscription != null) { final JSONObject jsonSub = new JSONObject(); try { jsonSub.put("url", subscription.url); jsonSub.put("title", subscription.title); jsonSub.put("homepage", subscription.homepage); js.execute(new Runnable() { @Override public void run() { js.evaluate("clearSubscriptions()"); js.evaluate("addSubscription(\"" + StringEscapeUtils.escapeJavaScript(jsonSub.toString()) + "\")"); } }); } catch (JSONException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } } } /** * Forces subscriptions refresh. */ public void refreshSubscription() { js.execute(new Runnable() { @Override public void run() { js.evaluate("refreshSubscriptions()"); } }); } /** * Selects which subscription to offer for the first time. * * @return offered subscription */ public Subscription offerSubscription() { Subscription selectedItem = null; String selectedPrefix = null; int matchCount = 0; for (Subscription subscription : getSubscriptions()) { if (selectedItem == null) selectedItem = subscription; String prefix = checkLocalePrefixMatch(subscription.prefixes); if (prefix != null) { if (selectedPrefix == null || selectedPrefix.length() < prefix.length()) { selectedItem = subscription; selectedPrefix = prefix; matchCount = 1; } else if (selectedPrefix != null && selectedPrefix.length() == prefix.length()) { matchCount++; // If multiple items have a matching prefix of the // same length select one of the items randomly, // probability should be the same for all items. // So we replace the previous match here with // probability 1/N (N being the number of matches). if (Math.random() * matchCount < 1) { selectedItem = subscription; selectedPrefix = prefix; } } } } return selectedItem; } /** * Verifies that subscriptions are loaded and returns flag of subscription * presence. * * @return true if at least one subscription is present and downloaded */ public boolean verifySubscriptions() { Future<Boolean> future = js.submit(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Boolean result = (Boolean) js.evaluate("verifySubscriptions()"); return result; } }); try { return future.get().booleanValue(); } catch (InterruptedException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } catch (ExecutionException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } return false; } /** * Returns ElemHide selectors for domain. * * @return ready to use HTML element with CSS selectors */ public String getSelectorsForDomain(final String domain) { if (!filteringEnabled) return null; Future<String> future = js.submit(new Callable<String>() { @Override public String call() throws Exception { String result = (String) js.evaluate("ElemHide.getSelectorsForDomain('" + domain + "')"); return result; } }); try { return future.get(); } catch (InterruptedException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } catch (ExecutionException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } return null; } private class MatchesCallable implements Callable<Boolean> { private String url; private String query; private String reqHost; private String refHost; private String accept; MatchesCallable(String url, String query, String reqHost, String refHost, String accept) { this.url = StringEscapeUtils.escapeJavaScript(url); this.query = StringEscapeUtils.escapeJavaScript(query); this.reqHost = reqHost != null ? StringEscapeUtils.escapeJavaScript(reqHost) : ""; this.refHost = refHost != null ? StringEscapeUtils.escapeJavaScript(refHost) : ""; this.accept = accept != null ? StringEscapeUtils.escapeJavaScript(accept) : ""; } @Override public Boolean call() throws Exception { Boolean result = (Boolean) js.evaluate("matchesAny('" + url + "', '" + query + "', '" + reqHost + "', '" + refHost + "', '" + accept + "');"); return result; } } /** * Checks if filters match request parameters. * * @param url * Request URL * @param query * Request query string * @param reqHost * Request host * @param refHost * Request referrer header * @param accept * Request accept header * @return true if matched filter was found * @throws Exception */ public boolean matches(String url, String query, String reqHost, String refHost, String accept) throws Exception { if (!filteringEnabled) return false; Callable<Boolean> callable = new MatchesCallable(url, query, reqHost, refHost, accept); Future<Boolean> future = js.submit(callable); boolean matches = future.get().booleanValue(); sendBroadcast(new Intent(BROADCAST_FILTER_MATCHES).putExtra("url", url).putExtra("matches", matches)); return matches; } /** * Checks if filtering is enabled. */ public boolean isFilteringEnabled() { return filteringEnabled; } /** * Enables or disables filtering. */ public void setFilteringEnabled(boolean enable) { filteringEnabled = enable; sendBroadcast(new Intent(BROADCAST_FILTERING_CHANGE).putExtra("enabled", filteringEnabled)); } /** * Notifies JS code that application entered interactive mode. */ public void startInteractive() { js.execute(new Runnable() { @Override public void run() { js.evaluate("startInteractive()"); } }); interactive = true; } /** * Notifies JS code that application quit interactive mode. */ public void stopInteractive() { // onStop is sometimes called without prior calling onStart // by Android system if (js == null) return; js.execute(new Runnable() { @Override public void run() { js.evaluate("stopInteractive()"); } }); interactive = false; } /** * Returns prefixes that match current user locale. */ public String checkLocalePrefixMatch(String[] prefixes) { if (prefixes == null || prefixes.length == 0) return null; String locale = Locale.getDefault().toString().toLowerCase(); for (int i = 0; i < prefixes.length; i++) if (locale.startsWith(prefixes[i].toLowerCase())) return prefixes[i]; return null; } /** * Starts JS engine. It also initiates subscription refresh if it is enabled * in user settings. */ public void startEngine() { if (js == null) { Log.i(TAG, "startEngine"); js = new JSThread(this); js.start(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); final int refresh = Integer.valueOf(prefs.getString(getString(R.string.pref_refresh), Integer.toString(getResources().getInteger(R.integer.def_refresh)))); final boolean wifionly = prefs.getBoolean(getString(R.string.pref_wifirefresh), getResources().getBoolean(R.bool.def_wifirefresh)); // Refresh if user selected refresh on each start if (refresh == 1 && (!wifionly || isWiFiConnected(this))) { refreshSubscription(); } } } /** * Stops JS engine. * * @param implicitly * stop even in interactive mode */ public void stopEngine(boolean implicitly) { if ((implicitly || !interactive) && js != null) { Log.i(TAG, "stopEngine"); js.stopEngine(); try { js.join(); } catch (InterruptedException e) { Log.e(TAG, e.getMessage(), e); } Log.i(TAG, "Engine stopped"); js = null; } } /** * Sets Alarm to call updater after specified number of minutes or after one * day if * minutes are set to 0. * * @param minutes * number of minutes to wait */ public void scheduleUpdater(int minutes) { Calendar updateTime = Calendar.getInstance(); if (minutes == 0) { // Start update checks at 10:00 GMT... updateTime.setTimeZone(TimeZone.getTimeZone("GMT")); updateTime.set(Calendar.HOUR_OF_DAY, 10); updateTime.set(Calendar.MINUTE, 0); // ...next day updateTime.add(Calendar.HOUR_OF_DAY, 24); // Spread out the mass downloading? for 6 hours updateTime.add(Calendar.MINUTE, (int) (Math.random() * 60 * 6)); } else { updateTime.add(Calendar.MINUTE, minutes); } Intent updater = new Intent(this, AlarmReceiver.class); PendingIntent recurringUpdate = PendingIntent.getBroadcast(this, 0, updater, PendingIntent.FLAG_CANCEL_CURRENT); // Set non-waking alarm AlarmManager alarms = (AlarmManager) getSystemService(Context.ALARM_SERVICE); alarms.set(AlarmManager.RTC, updateTime.getTimeInMillis(), recurringUpdate); } @Override public void onCreate() { super.onCreate(); instance = this; // Check for crash report try { InputStreamReader reportFile = new InputStreamReader(openFileInput(CrashHandler.REPORT_FILE)); final char[] buffer = new char[0x1000]; StringBuilder out = new StringBuilder(); int read; do { read = reportFile.read(buffer, 0, buffer.length); if (read > 0) out.append(buffer, 0, read); } while (read >= 0); String report = out.toString(); if (!"".equals(report)) { final Intent intent = new Intent(this, CrashReportDialog.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("report", report); startActivity(intent); } } catch (FileNotFoundException e) { // ignore } catch (IOException e) { // TODO Auto-generated catch block Log.e(TAG, e.getMessage(), e); } // Set crash handler Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this)); // Initiate update check scheduleUpdater(0); } /** * Handler for showing toast messages from JS code. */ private static final Handler messageHandler = new Handler() { public void handleMessage(Message msg) { if (msg.what == MSG_TOAST) { Toast.makeText(AdblockPlus.getApplication(), msg.getData().getString("message"), Toast.LENGTH_LONG) .show(); } } }; /** * JS execution thread. */ private final class JSThread extends Thread { private JSEngine jsEngine; private volatile boolean run = true; private Context context; private final LinkedList<Runnable> queue = new LinkedList<Runnable>(); private long delay = -1; JSThread(Context context) { this.context = context; } // JS helper @SuppressWarnings("unused") public String readJSFile(String name) { String result = ""; AssetManager assetManager = getAssets(); try { InputStreamReader reader = new InputStreamReader(assetManager.open("js" + File.separator + name)); final char[] buffer = new char[0x10000]; StringBuilder out = new StringBuilder(); int read; do { read = reader.read(buffer, 0, buffer.length); if (read > 0) out.append(buffer, 0, read); } while (read >= 0); result = out.toString(); } catch (IOException e) { Log.e(TAG, e.getMessage(), e); } return result; } // JS helper public FileInputStream getInputStream(String path) { Log.d(TAG, path); File f = new File(path); try { return openFileInput(f.getName()); } catch (FileNotFoundException e) { Log.e(TAG, e.getMessage(), e); } return null; } // JS helper public FileOutputStream getOutputStream(String path) { Log.d(TAG, path); File f = new File(path); try { return openFileOutput(f.getName(), MODE_PRIVATE); } catch (FileNotFoundException e) { Log.e(TAG, e.getMessage(), e); } return null; } // JS helper public String getVersion() { String versionName = null; try { versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; } catch (NameNotFoundException ex) { versionName = "n/a"; } return versionName; } // JS helper @SuppressWarnings("unused") public boolean canAutoupdate() { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final int refresh = Integer.valueOf(prefs.getString(getString(R.string.pref_refresh), Integer.toString(context.getResources().getInteger(R.integer.def_refresh)))); final boolean wifionly = prefs.getBoolean(getString(R.string.pref_wifirefresh), getResources().getBoolean(R.bool.def_wifirefresh)); return refresh == 2 && (!wifionly || isWiFiConnected(context)); } // JS helper @SuppressWarnings("unused") public void httpSend(final String method, final String url, final String[][] headers, final boolean async, final long callback) { Log.d(TAG, "httpSend('" + method + "', '" + url + "')"); messageHandler.post(new Runnable() { @Override public void run() { try { Task task = new Task(); task.callback = callback; task.connection = (HttpURLConnection) new URL(url).openConnection(); task.connection.setRequestMethod(method); for (int i = 0; i < headers.length; i++) { task.connection.setRequestProperty(headers[i][0], headers[i][1]); } DownloadTask downloadTask = new DownloadTask(context); downloadTask.execute(task); if (!async) { downloadTask.get(); } } catch (Exception e) { Log.e(TAG, e.getMessage(), e); js.callback(callback, null); } } }); } // JS helper @SuppressWarnings("unused") public void setStatus(String text, long time) { sendBroadcast(new Intent(BROADCAST_SUBSCRIPTION_STATUS).putExtra("text", text).putExtra("time", time)); } // JS helper @SuppressWarnings("unused") public void showToast(String text) { Log.d(TAG, "Toast: " + text); Message msg = messageHandler.obtainMessage(MSG_TOAST); Bundle data = new Bundle(); data.putString("message", text); msg.setData(data); messageHandler.sendMessage(msg); } // JS helper @SuppressWarnings("unused") public void notify(long delay) { if (this.delay < 0 || delay < this.delay) { this.delay = delay; synchronized (queue) { queue.notify(); } } } public Object evaluate(String script) { return jsEngine.evaluate(script); } public void callback(long callback, Object[] params) { jsEngine.callback(callback, params); } public final void stopEngine() { run = false; synchronized (queue) { queue.notify(); } } public void execute(Runnable r) { synchronized (queue) { queue.addLast(r); queue.notify(); } } public <T> Future<T> submit(Callable<T> callable) { FutureTask<T> ftask = new FutureTask<T>(callable); execute(ftask); return ftask; } @Override public final void run() { if (Thread.currentThread().getName().startsWith("Thread-")) { Thread.currentThread().setName("javascript"); } jsEngine = new JSEngine(this); jsEngine.put("_locale", Locale.getDefault().toString()); jsEngine.put("_datapath", getFilesDir().getAbsolutePath()); jsEngine.put("_separator", File.separator); jsEngine.put("_version", getVersion()); try { jsEngine.evaluate("Android.load(\"start.js\");"); } catch (Exception e) { Log.e(TAG, e.getMessage(), e); } while (run) { try { Runnable r = null; synchronized (queue) { r = queue.poll(); } if (r != null) { r.run(); } else if (delay > 0) { long t = SystemClock.uptimeMillis(); synchronized (queue) { try { queue.wait(delay); } catch (InterruptedException e) { } } delay -= SystemClock.uptimeMillis() - t; } else if (delay <= 0) { delay = jsEngine.runCallbacks(); } else { synchronized (queue) { try { queue.wait(); } catch (InterruptedException e) { Log.e(TAG, e.getMessage(), e); } } } } catch (Exception e) { Log.e(TAG, e.getMessage(), e); } } jsEngine.release(); } } /** * Helper class for XMLHttpRequest implementation. */ private class Task { HttpURLConnection connection; long callback; } /** * Helper class for XMLHttpRequest implementation. */ private class Result { long callback; int code; String message; String data; Map<String, List<String>> headers; } /** * Helper class for XMLHttpRequest implementation. */ private class DownloadTask extends AsyncTask<Task, Integer, Result> { public DownloadTask(Context context) { } @Override protected void onPreExecute() { } @Override protected void onPostExecute(Result result) { if (result != null) { final long callback = result.callback; final Object[] params = new Object[4]; String[][] headers = null; if (result.headers != null) { headers = new String[result.headers.size()][2]; int i = 0; for (String header : result.headers.keySet()) { headers[i][0] = header; headers[i][1] = StringUtils.join(result.headers.get(header).toArray(), "; "); i++; } } params[0] = result.code; params[1] = result.message; params[2] = headers; params[3] = result.data; // Do not run callback if engine was stopped if (js == null) return; js.execute(new Runnable() { @Override public void run() { js.callback(callback, params); } }); } } @Override protected void onCancelled() { } @Override protected Result doInBackground(Task... tasks) { Task task = tasks[0]; Result result = new Result(); result.callback = task.callback; try { HttpURLConnection connection = task.connection; connection.connect(); int lenghtOfFile = connection.getContentLength(); Log.d("D", "S: " + lenghtOfFile); result.code = connection.getResponseCode(); result.message = connection.getResponseMessage(); result.headers = connection.getHeaderFields(); // download the file String encoding = connection.getContentEncoding(); if (encoding == null) encoding = "utf-8"; BufferedReader in = new BufferedReader( new InputStreamReader(connection.getInputStream(), encoding)); final char[] buffer = new char[0x10000]; StringBuilder out = new StringBuilder(); long total = 0; int read; do { read = in.read(buffer, 0, buffer.length); if (read > 0) { out.append(buffer, 0, read); total += read; publishProgress((int) (total * 100. / lenghtOfFile)); } } while (!isCancelled() && read >= 0); result.data = out.toString(); in.close(); } catch (Exception e) { Log.e(TAG, e.getMessage(), e); result.data = ""; result.code = HttpURLConnection.HTTP_INTERNAL_ERROR; result.message = e.toString(); } return result; } protected void onProgressUpdate(Integer... progress) { Log.d("HTTP", "Progress: " + progress[0].intValue()); } } }