| Index: src/org/adblockplus/android/AdblockPlus.java |
| =================================================================== |
| new file mode 100755 |
| --- /dev/null |
| +++ b/src/org/adblockplus/android/AdblockPlus.java |
| @@ -0,0 +1,1056 @@ |
| +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.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.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.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.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 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; |
| + |
| + private boolean generateCrashReport = 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; |
| + } |
| + |
| + /** |
| + * 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; |
| + } |
| + |
| + 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 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) |
| + { |
| + 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 = url; |
| + this.query = query; |
| + this.reqHost = reqHost != null ? reqHost : ""; |
| + this.refHost = refHost != null ? refHost : ""; |
| + this.accept = accept != null ? 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 |
| + { |
| + 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; |
| + } |
| + |
| + /** |
| + * 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() |
| + { |
| + 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) |
| + { |
| + js.stopEngine(); |
| + try |
| + { |
| + js.join(); |
| + } |
| + catch (InterruptedException e) |
| + { |
| + Log.e(TAG, e.getMessage(), e); |
| + } |
| + js = null; |
| + } |
| + } |
| + |
| + /** |
| + * Sets or removes crash handler according to user setting |
| + */ |
| + public void updateCrashReportStatus() |
| + { |
| + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); |
| + boolean report = prefs.getBoolean(getString(R.string.pref_crashreport), getResources().getBoolean(R.bool.def_crashreport)); |
| + if (report != generateCrashReport) |
| + { |
| + if (report) |
| + { |
| + Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this)); |
| + } |
| + else |
| + { |
| + try |
| + { |
| + CrashHandler handler = (CrashHandler) Thread.getDefaultUncaughtExceptionHandler(); |
| + Thread.setDefaultUncaughtExceptionHandler(handler.getDefault()); |
| + } |
| + catch (ClassCastException e) |
| + { |
| + // ignore - already default handler |
| + } |
| + } |
| + generateCrashReport = report; |
| + } |
| + } |
| + |
| + /** |
| + * 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); |
| + } |
| + |
| + if (!getResources().getBoolean(R.bool.def_release)) |
| + { |
| + // Set crash handler |
| + updateCrashReportStatus(); |
| + // 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() |
| + { |
| + 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; |
| + 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()); |
| + } |
| + } |
| +} |