| Index: adblockplussbrowser/src/main/java/org/adblockplus/sbrowser/contentblocker/engine/Engine.java |
| =================================================================== |
| new file mode 100644 |
| --- /dev/null |
| +++ b/adblockplussbrowser/src/main/java/org/adblockplus/sbrowser/contentblocker/engine/Engine.java |
| @@ -0,0 +1,819 @@ |
| +/* |
| + * This file is part of Adblock Plus <https://adblockplus.org/>, |
| + * Copyright (C) 2006-present 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.sbrowser.contentblocker.engine; |
| + |
| +import java.io.BufferedReader; |
| +import java.io.BufferedWriter; |
| +import java.io.File; |
| +import java.io.FileOutputStream; |
| +import java.io.IOException; |
| +import java.io.InputStream; |
| +import java.io.InputStreamReader; |
| +import java.io.OutputStreamWriter; |
| +import java.io.Writer; |
| +import java.net.URI; |
| +import java.net.URISyntaxException; |
| +import java.net.URL; |
| +import java.net.URLEncoder; |
| +import java.nio.charset.StandardCharsets; |
| +import java.util.ArrayList; |
| +import java.util.Collections; |
| +import java.util.HashMap; |
| +import java.util.List; |
| +import java.util.Map; |
| +import java.util.Set; |
| +import java.util.TreeSet; |
| +import java.util.concurrent.LinkedBlockingQueue; |
| +import java.util.concurrent.TimeUnit; |
| +import java.util.concurrent.locks.ReentrantLock; |
| + |
| +import org.adblockplus.adblockplussbrowser.R; |
| +import org.adblockplus.sbrowser.contentblocker.util.SharedPrefsUtils; |
| +import org.adblockplus.sbrowser.contentblocker.util.SubscriptionUtils; |
| + |
| +import android.content.Context; |
| +import android.content.Intent; |
| +import android.content.pm.PackageInfo; |
| +import android.content.pm.PackageManager; |
| +import android.content.pm.ResolveInfo; |
| +import android.net.ConnectivityManager; |
| +import android.net.NetworkInfo; |
| +import android.net.Uri; |
| +import android.os.Handler; |
| +import android.os.Looper; |
| +import android.support.annotation.VisibleForTesting; |
| +import android.text.TextUtils; |
| +import android.text.format.DateUtils; |
| +import android.util.Log; |
| + |
| +public final class Engine |
| +{ |
| + private static final String TAG = Engine.class.getSimpleName(); |
| + |
| + public static final String USER_FILTERS_TITLE = "__filters"; |
| + public static final String USER_EXCEPTIONS_TITLE = "__exceptions"; |
| + |
| + public static final String SBROWSER_APP_ID = "com.sec.android.app.sbrowser"; |
| + public static final String EASYLIST_URL = "https://easylist-downloads.adblockplus.org/easylist.txt"; |
| + private static final String ACTION_OPEN_SETTINGS = "com.samsung.android.sbrowser.contentBlocker.ACTION_SETTING"; |
| + private static final String ACTION_UPDATE = "com.samsung.android.sbrowser.contentBlocker.ACTION_UPDATE"; |
| + |
| + public static final String SUBSCRIPTIONS_EXCEPTIONSURL = "subscriptions_exceptionsurl"; |
| + |
| + // The value below specifies an interval of [x, 2*x[, where x = |
| + // INITIAL_UPDATE_CHECK_DELAY |
| + private static final long INITIAL_UPDATE_CHECK_DELAY = 5 * DateUtils.SECOND_IN_MILLIS; |
| + private static final long UPDATE_CHECK_INTERVAL = 30 * DateUtils.MINUTE_IN_MILLIS; |
| + private static final long BROADCAST_COMBINATION_DELAY = 2500; |
| + |
| + private static final int NO_FLAG = 0; |
| + private static final int OLDEST_SAMSUNG_INTERNET_5_VERSIONCODE = 500000000; |
| + |
| + private final ReentrantLock accessLock = new ReentrantLock(); |
| + private DefaultSubscriptions defaultSubscriptions; |
| + private Subscriptions subscriptions; |
| + private JSONPrefs jsonPrefs; |
| + private AppInfo appInfo; |
| + private final LinkedBlockingQueue<EngineEvent> engineEvents = new LinkedBlockingQueue<>(); |
| + private Thread handlerThread; |
| + private Downloader downloader; |
| + private SubscriptionUpdateCallback subscriptionUpdateCallback; |
| + private final Context serviceContext; |
| + private boolean wasFirstRun = false; |
| + private long nextUpdateBroadcast = Long.MAX_VALUE; |
| + |
| + private Engine(final Context context) |
| + { |
| + this.serviceContext = context; |
| + } |
| + |
| + public String getPrefsDefault(final String key) |
| + { |
| + return this.jsonPrefs.getDefaults(key); |
| + } |
| + |
| + DefaultSubscriptionInfo getDefaultSubscriptionInfo(final Subscription sub) |
| + { |
| + return this.defaultSubscriptions.getForUrl(sub.getURL()); |
| + } |
| + |
| + void lock() |
| + { |
| + this.accessLock.lock(); |
| + } |
| + |
| + void unlock() |
| + { |
| + this.accessLock.unlock(); |
| + } |
| + |
| + public static boolean openSBrowserSettings(final Context activityContext) |
| + { |
| + final Intent intent = new Intent(ACTION_OPEN_SETTINGS); |
| + final List<ResolveInfo> list = activityContext.getPackageManager() |
| + .queryIntentActivities(intent, 0); |
| + if (list.size() > 0) |
| + { |
| + activityContext.startActivity(intent); |
| + } |
| + return list.size() > 0; |
| + } |
| + |
| + public static boolean hasCompatibleSBrowserInstalled(final Context activityContext) |
| + { |
| + try |
| + { |
| + return activityContext.getPackageManager() |
| + .queryIntentActivities(new Intent(ACTION_OPEN_SETTINGS), 0).size() > 0; |
| + } |
| + catch (final Throwable t) |
| + { |
| + return false; |
| + } |
| + } |
| + |
| + /** |
| + * Starting with Samsung Internet 5.0, the way to enable ad blocking has changed. As a result, we |
| + * need to check for the version of Samsung Internet and apply text changes to the first run slide. |
| + * |
| + * @param activityContext |
| + * @return a boolean that indicates, if the user has Samsung Internet version 5.x |
| + */ |
| + public static boolean hasSamsungInternetVersion5OrNewer(final Context activityContext) |
| + { |
| + try |
| + { |
| + PackageInfo packageInfo = activityContext.getPackageManager().getPackageInfo(SBROWSER_APP_ID, NO_FLAG); |
| + return packageInfo.versionCode >= OLDEST_SAMSUNG_INTERNET_5_VERSIONCODE; |
| + } |
| + catch (PackageManager.NameNotFoundException e) |
| + { |
| + // Should never happen, as checkAAStatusAndProceed() should not be called if the user |
| + // has no compatible SBrowser installed. Nevertheless we have to handle the Exception. |
| + Log.d(TAG, "No compatible Samsung Browser found.", e); |
| + return false; |
| + } |
| + } |
| + |
| + public void setSubscriptionUpdateCallback(final SubscriptionUpdateCallback subscriptionUpdateCallback) |
| + { |
| + this.subscriptionUpdateCallback = subscriptionUpdateCallback; |
| + } |
| + |
| + public void requestUpdateBroadcast() |
| + { |
| + this.nextUpdateBroadcast = System.currentTimeMillis() + BROADCAST_COMBINATION_DELAY; |
| + } |
| + |
| + private void writeFileAndSendUpdateBroadcast() |
| + { |
| + createAndWriteFile(); |
| + |
| + runOnUiThread(new Runnable() |
| + { |
| + @Override |
| + public void run() |
| + { |
| + final Intent intent = new Intent(); |
| + intent.setAction(ACTION_UPDATE); |
| + intent.setData(Uri.parse("package:" + Engine.this.serviceContext.getPackageName())); |
| + Engine.this.serviceContext.sendBroadcast(intent); |
| + } |
| + }); |
| + } |
| + |
| + boolean canUseInternet() |
| + { |
| + final ConnectivityManager connManager = (ConnectivityManager) this.serviceContext |
| + .getSystemService(Context.CONNECTIVITY_SERVICE); |
| + final NetworkInfo current = connManager.getActiveNetworkInfo(); |
| + if (current == null) |
| + { |
| + return false; |
| + } |
| + |
| + if (wasFirstRun()) |
| + { |
| + return true; |
| + } |
| + |
| + final boolean wifiOnly = "1".equals(SharedPrefsUtils.getString( |
| + this.serviceContext, R.string.key_automatic_updates , "1")); |
| + |
| + if (wifiOnly) |
| + { |
| + if (current.isConnected() && !current.isRoaming()) |
| + { |
| + switch (current.getType()) |
| + { |
| + case ConnectivityManager.TYPE_BLUETOOTH: |
| + case ConnectivityManager.TYPE_ETHERNET: |
| + case ConnectivityManager.TYPE_WIFI: |
| + case ConnectivityManager.TYPE_WIMAX: |
| + return true; |
| + default: |
| + return false; |
| + } |
| + } |
| + return false; |
| + } |
| + return current.isConnected(); |
| + } |
| + |
| + public List<SubscriptionInfo> getListedSubscriptions() |
| + { |
| + return this.subscriptions.getSubscriptions(this); |
| + } |
| + |
| + public void changeSubscriptionState(final String id, final boolean enabled) |
| + { |
| + if (this.subscriptionUpdateCallback != null) |
| + { |
| + subscriptionUpdateCallback.subscriptionUpdateRequested(enabled); |
| + } |
| + this.engineEvents.add(new ChangeEnabledStateEvent(id, enabled)); |
| + } |
| + |
| + public void subscriptionStateChanged() |
| + { |
| + if (this.subscriptionUpdateCallback != null) |
| + { |
| + subscriptionUpdateCallback.subscriptionUpdatedApplied(); |
| + } |
| + } |
| + |
| + public void createAndAddSubscriptionFromUrl(final String url, |
| + final SubscriptionAddedCallback callback) throws IOException |
| + { |
| + final Subscription sub = Subscription.create(url); |
| + sub.putMeta(Subscription.KEY_TITLE, url); |
| + sub.setEnabled(true); |
| + subscriptions.add(sub); |
| + subscriptions.persistSubscription(sub); |
| + callback.subscriptionAdded(); |
| + } |
| + |
| + public void removeSubscriptionById(final String subscriptionId) |
| + { |
| + subscriptions.remove(subscriptionId); |
| + } |
| + |
| + void downloadFinished(final String id, final int responseCode, final String response, |
| + final Map<String, String> headers) |
| + { |
| + this.engineEvents.add(new DownloadFinishedEvent(id, responseCode, response, headers)); |
| + } |
| + |
| + private void createAndWriteFile() |
| + { |
| + this.lock(); |
| + try |
| + { |
| + Log.d(TAG, "Writing filters..."); |
| + final File filterFile = this.subscriptions.createAndWriteFile(); |
| + writeWhitelistedWebsites(this.serviceContext, filterFile); |
| + |
| + SharedPrefsUtils.putString( |
| + this.serviceContext, R.string.key_cached_filter_path, filterFile.getAbsolutePath()); |
| + |
| + Log.d(TAG, "Cleaning up cache..."); |
| + final File dummyFile = getDummyFilterFile(this.serviceContext); |
| + final File[] cacheDirFiles = getFilterCacheDir(this.serviceContext).listFiles(); |
| + if (cacheDirFiles != null) |
| + { |
| + for (final File file : cacheDirFiles) |
| + { |
| + if (!file.equals(dummyFile) && !file.equals(filterFile)) |
| + { |
| + Log.d(TAG, "Deleting file:" + file); |
| + file.delete(); |
| + } |
| + } |
| + } |
| + } |
| + catch (IOException e) |
| + { |
| + Log.e(TAG, "Failed to write filters", e); |
| + } |
| + finally |
| + { |
| + this.unlock(); |
| + } |
| + } |
| + |
| + public static void runOnUiThread(final Runnable runnable) |
| + { |
| + new Handler(Looper.getMainLooper()).post(runnable); |
| + } |
| + |
| + public boolean isAcceptableAdsEnabled() |
| + { |
| + this.lock(); |
| + try |
| + { |
| + return this.subscriptions.isSubscriptionEnabled("url:" |
| + + this.getPrefsDefault(SUBSCRIPTIONS_EXCEPTIONSURL)); |
| + } |
| + finally |
| + { |
| + this.unlock(); |
| + } |
| + } |
| + |
| + public DefaultSubscriptionInfo getDefaultSubscriptionInfoForUrl(final String url) |
| + { |
| + return this.defaultSubscriptions.getForUrl(url); |
| + } |
| + |
| + /** |
| + * If the user starts the app for the first time, we force to update the subscription which was |
| + * selected as the default, no matter if he has a WIFI connection or not. From the second start |
| + * we only update when the user has a WIFI connection. |
| + * |
| + * @return a boolean that indicated if this is the first start of the app |
| + */ |
| + @VisibleForTesting |
| + private boolean wasFirstRun() |
| + { |
| + if (wasFirstRun) |
| + { |
| + this.wasFirstRun = false; |
| + return true; |
| + } |
| + else |
| + { |
| + return false; |
| + } |
| + } |
| + |
| + private void migrateFromPreviousVersion(final Context context) |
| + { |
| + try |
| + { |
| + final int versionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), |
| + 0).versionCode; |
| + |
| + final int previousVersionCode = SharedPrefsUtils.getInt( |
| + context, R.string.key_previous_version_code, 0); |
| + |
| + if (versionCode > previousVersionCode) |
| + { |
| + if (previousVersionCode > 0) |
| + { |
| + // We can do possible migration stuff here |
| + // Currently we only persist the new version code |
| + } |
| + SharedPrefsUtils.putInt(context, R.string.key_previous_version_code, versionCode); |
| + } |
| + } |
| + catch (final Throwable t) |
| + { |
| + Log.e(TAG, "Failed on migration, please clear all application data", t); |
| + } |
| + } |
| + |
| + static Engine create(final Context context) throws IOException |
| + { |
| + final Engine engine = new Engine(context); |
| + |
| + // Migration data from previous version (if needed) |
| + engine.migrateFromPreviousVersion(context); |
| + Log.d(TAG, "Migration done"); |
| + |
| + engine.appInfo = AppInfo.create(context); |
| + |
| + Log.d(TAG, "Creating engine, appInfo=" + engine.appInfo.toString()); |
| + |
| + try (final InputStream subscriptionsXml = context.getResources() |
| + .openRawResource(R.raw.subscriptions)) |
| + { |
| + engine.defaultSubscriptions = DefaultSubscriptions.fromStream(subscriptionsXml); |
| + } |
| + |
| + Log.d(TAG, "Finished reading 'subscriptions.xml'"); |
| + engine.subscriptions = Subscriptions.initialize(engine, getSubscriptionsDir(context), |
| + getFilterCacheDir(context)); |
| + |
| + try (final InputStream prefsJson = context.getResources().openRawResource(R.raw.prefs)) |
| + { |
| + engine.jsonPrefs = JSONPrefs.create(prefsJson); |
| + } |
| + |
| + Log.d(TAG, "Finished reading JSON preferences"); |
| + |
| + // Check if this is a fresh start, if so: initialize bundled easylist. |
| + engine.wasFirstRun = engine.subscriptions.wasUnitialized(); |
| + if (engine.subscriptions.wasUnitialized()) |
| + { |
| + Log.d(TAG, "Subscription storage was uninitialized, initializing..."); |
| + |
| + try (final InputStream easylistTxt = context.getResources().openRawResource(R.raw.easylist)) |
| + { |
| + final Subscription easylist = engine.subscriptions.add(Subscription |
| + // Use bundled EasyList as default and update it with locale specific list later |
| + // see: https://issues.adblockplus.org/ticket/5237 |
| + .create(SubscriptionUtils.chooseDefaultSubscriptionUrl( |
| + engine.defaultSubscriptions.getAdsSubscriptions())) |
| + .parseLines(readLines(easylistTxt))); |
| + easylist.putMeta(Subscription.KEY_UPDATE_TIMESTAMP, "0"); |
| + easylist.setEnabled(true); |
| + } |
| + Log.d(TAG, "Added and enabled bundled easylist"); |
| + |
| + try (final InputStream exceptionsTxt = context.getResources() |
| + .openRawResource(R.raw.exceptionrules)) |
| + { |
| + final Subscription exceptions = engine.subscriptions.add(Subscription |
| + .create(engine.getPrefsDefault(SUBSCRIPTIONS_EXCEPTIONSURL)) |
| + .parseLines(readLines(exceptionsTxt))); |
| + exceptions.putMeta(Subscription.KEY_UPDATE_TIMESTAMP, "0"); |
| + exceptions.setEnabled(true); |
| + } |
| + Log.d(TAG, "Added and enabled bundled exceptionslist"); |
| + |
| + int additional = 0; |
| + for (final Subscription sub : engine.defaultSubscriptions.createSubscriptions()) |
| + { |
| + if (!engine.subscriptions.hasSubscription(sub.getId())) |
| + { |
| + additional++; |
| + engine.subscriptions.add(sub); |
| + } |
| + } |
| + |
| + Log.d(TAG, "Added " + additional + " additional default/built-in subscriptions"); |
| + engine.subscriptions.persistSubscriptions(); |
| + } |
| + |
| + engine.handlerThread = new Thread(new EventHandler(engine)); |
| + engine.handlerThread.setDaemon(true); |
| + engine.handlerThread.start(); |
| + |
| + engine.downloader = Downloader.create(engine); |
| + |
| + final File cachedFilterFile = getCachedFilterFile(context); |
| + if (cachedFilterFile == null || !cachedFilterFile.exists()) |
| + { |
| + engine.writeFileAndSendUpdateBroadcast(); |
| + } |
| + |
| + return engine; |
| + } |
| + |
| + public static String readFileAsString(InputStream instream) throws IOException |
| + { |
| + final StringBuilder sb = new StringBuilder(); |
| + try (final BufferedReader r = new BufferedReader(new InputStreamReader( |
| + instream, StandardCharsets.UTF_8))) |
| + { |
| + for (int ch = r.read(); ch != -1; ch = r.read()) |
| + { |
| + sb.append((char) ch); |
| + } |
| + } |
| + return sb.toString(); |
| + } |
| + |
| + public static List<String> readLines(InputStream instream) throws IOException |
| + { |
| + final ArrayList<String> list = new ArrayList<>(); |
| + try (final BufferedReader r = new BufferedReader(new InputStreamReader( |
| + instream, StandardCharsets.UTF_8))) |
| + { |
| + for (String line = r.readLine(); line != null; line = r.readLine()) |
| + { |
| + list.add(line); |
| + } |
| + } |
| + return list; |
| + } |
| + |
| + public static File getOrCreateCachedFilterFile(Context context) throws IOException |
| + { |
| + final File cachedFilterFile = getCachedFilterFile(context); |
| + if (cachedFilterFile != null && cachedFilterFile.exists()) |
| + { |
| + Log.d(TAG, "Cached filter file found: " + cachedFilterFile); |
| + return cachedFilterFile; |
| + } |
| + |
| + Log.d(TAG, "Cached filter file not found. Using dummy filter file"); |
| + final File dummyFilterFile = getDummyFilterFile(context); |
| + if (!dummyFilterFile.exists()) |
| + { |
| + Log.d(TAG, "Creating dummy filter file..."); |
| + dummyFilterFile.getParentFile().mkdirs(); |
| + try (final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( |
| + new FileOutputStream(dummyFilterFile), StandardCharsets.UTF_8))) |
| + { |
| + writeFilterHeaders(writer); |
| + } |
| + } |
| + return dummyFilterFile; |
| + } |
| + |
| + public static void writeFilterHeaders(Writer writer) throws IOException |
| + { |
| + writer.write("[Adblock Plus 2.0]\n"); |
| + writer.write("! This file was automatically created.\n"); |
| + } |
| + |
| + private static void writeWhitelistedWebsites(Context context, File filterFile) throws IOException |
| + { |
| + Log.d(TAG, "Writing whitelisted websites..."); |
| + final Set<String> whitelistedWebsites = new TreeSet<>(); |
| + whitelistedWebsites.addAll(SharedPrefsUtils.getStringSet( |
| + context, R.string.key_whitelisted_websites, Collections.<String>emptySet())); |
| + |
| + try (final BufferedWriter w = new BufferedWriter( new OutputStreamWriter( |
| + new FileOutputStream(filterFile, true), StandardCharsets.UTF_8))) |
| + { |
| + for (final String url : whitelistedWebsites) |
| + { |
| + try |
| + { |
| + final URI uri = new URI(url); |
| + final String host = uri.getHost() != null ? uri.getHost() : uri.getPath(); |
| + w.write("@@||" + host + "^$document"); |
| + w.write('\n'); |
| + } |
| + catch (URISyntaxException e) |
| + { |
| + Log.w(TAG, "Failed to parse whitelisted website: " + url); |
| + } |
| + } |
| + } |
| + } |
| + |
| + private static File getCachedFilterFile(Context context) |
| + { |
| + final String cachedFilterPath = SharedPrefsUtils.getString( |
| + context, R.string.key_cached_filter_path, null); |
| + |
| + if (cachedFilterPath != null) |
| + { |
| + return new File(cachedFilterPath); |
| + } |
| + |
| + return null; |
| + } |
| + |
| + private static File getDummyFilterFile(Context context) |
| + { |
| + return new File(getFilterCacheDir(context), "dummy.txt"); |
| + } |
| + |
| + private static File getFilterCacheDir(Context context) |
| + { |
| + return new File(context.getCacheDir(), "subscriptions"); |
| + } |
| + |
| + private static File getSubscriptionsDir(Context context) |
| + { |
| + return new File(context.getFilesDir(), "subscriptions"); |
| + } |
| + |
| + URL createDownloadURL(final Subscription sub) throws IOException |
| + { |
| + final StringBuilder sb = new StringBuilder(); |
| + |
| + sb.append(sub.getURL()); |
| + if (sub.getURL().getQuery() != null) |
| + { |
| + sb.append('&'); |
| + } |
| + else |
| + { |
| + sb.append('?'); |
| + } |
| + |
| + sb.append("addonName="); |
| + sb.append(URLEncoder.encode(this.appInfo.addonName, StandardCharsets.UTF_8.name())); |
| + sb.append("&addonVersion="); |
| + sb.append(URLEncoder.encode(this.appInfo.addonVersion, StandardCharsets.UTF_8.name())); |
| + sb.append("&application="); |
| + sb.append(URLEncoder.encode(this.appInfo.application, StandardCharsets.UTF_8.name())); |
| + sb.append("&applicationVersion="); |
| + sb.append(URLEncoder.encode(this.appInfo.applicationVersion, StandardCharsets.UTF_8.name())); |
| + sb.append("&platform="); |
| + sb.append(URLEncoder.encode(this.appInfo.platform, StandardCharsets.UTF_8.name())); |
| + sb.append("&platformVersion="); |
| + sb.append(URLEncoder.encode(this.appInfo.platformVersion, StandardCharsets.UTF_8.name())); |
| + sb.append("&lastVersion="); |
| + sb.append(sub.getVersion()); |
| + sb.append("&downloadCount="); |
| + final long downloadCount = sub.getDownloadCount(); |
| + if (downloadCount < 5) |
| + { |
| + sb.append(downloadCount); |
| + } |
| + else |
| + { |
| + sb.append("4%2B"); // "4+" URL encoded |
| + } |
| + |
| + return new URL(sb.toString()); |
| + } |
| + |
| + public boolean isAcceptableAdsUrl(final SubscriptionInfo subscriptionInfo) |
| + { |
| + return getPrefsDefault(SUBSCRIPTIONS_EXCEPTIONSURL).equals(subscriptionInfo.getUrl()); |
| + } |
| + |
| + private static class EventHandler implements Runnable |
| + { |
| + private static final String TAG = EventHandler.class.getSimpleName(); |
| + private final Engine engine; |
| + |
| + public EventHandler(final Engine engine) |
| + { |
| + this.engine = engine; |
| + } |
| + |
| + @Override |
| + public void run() |
| + { |
| + Log.d(TAG, "Handler thread started"); |
| + boolean interrupted = false; |
| + long nextUpdateCheck = System.currentTimeMillis() |
| + + (long) ((1 + Math.random()) * INITIAL_UPDATE_CHECK_DELAY); |
| + while (!interrupted) |
| + { |
| + try |
| + { |
| + final EngineEvent event = this.engine.engineEvents.poll(100, TimeUnit.MILLISECONDS); |
| + engine.lock(); |
| + try |
| + { |
| + if (event != null) |
| + { |
| + switch (event.getType()) |
| + { |
| + case CHANGE_ENABLED_STATE: |
| + { |
| + final ChangeEnabledStateEvent cese = (ChangeEnabledStateEvent) event; |
| + Log.d(TAG, "Changing " + cese.id + " to enabled: " + cese.enabled); |
| + engine.subscriptions.changeSubscriptionState(cese.id, cese.enabled); |
| + break; |
| + } |
| + case DOWNLOAD_FINISHED: |
| + { |
| + final DownloadFinishedEvent dfe = (DownloadFinishedEvent) event; |
| + Log.d(TAG, "Download finished for '" + dfe.id + "' with response code " |
| + + dfe.responseCode); |
| + this.engine.subscriptions.updateSubscription(dfe.id, dfe.responseCode, |
| + dfe.response, dfe.headers); |
| + break; |
| + } |
| + default: |
| + Log.d(TAG, "Unhandled type: " + event.getType()); |
| + break; |
| + } |
| + } |
| + |
| + final long currentTime = System.currentTimeMillis(); |
| + if (currentTime > nextUpdateCheck) |
| + { |
| + nextUpdateCheck = currentTime + UPDATE_CHECK_INTERVAL; |
| + |
| + this.engine.subscriptions.checkForUpdates(); |
| + } |
| + |
| + if (currentTime > this.engine.nextUpdateBroadcast) |
| + { |
| + this.engine.nextUpdateBroadcast = Long.MAX_VALUE; |
| + Log.d(TAG, "Sending update broadcast"); |
| + this.engine.writeFileAndSendUpdateBroadcast(); |
| + } |
| + } |
| + finally |
| + { |
| + engine.unlock(); |
| + } |
| + } |
| + catch (final InterruptedException e) |
| + { |
| + Log.d(TAG, "Handler interrupted", e); |
| + interrupted = true; |
| + } |
| + catch (final Throwable t) |
| + { |
| + Log.e(TAG, "Event processing failed: " + t.getMessage(), t); |
| + } |
| + } |
| + Log.d(TAG, "Handler thread finished"); |
| + } |
| + } |
| + |
| + private static class EngineEvent |
| + { |
| + public enum EngineEventType |
| + { |
| + CHANGE_ENABLED_STATE, |
| + FORCE_DOWNLOAD, |
| + DOWNLOAD_FINISHED |
| + } |
| + |
| + private final EngineEventType type; |
| + |
| + EngineEvent(final EngineEventType type) |
| + { |
| + this.type = type; |
| + } |
| + |
| + public EngineEventType getType() |
| + { |
| + return this.type; |
| + } |
| + } |
| + |
| + private static class ChangeEnabledStateEvent extends EngineEvent |
| + { |
| + private final String id; |
| + private final boolean enabled; |
| + |
| + public ChangeEnabledStateEvent(final String id, final boolean enabled) |
| + { |
| + super(EngineEvent.EngineEventType.CHANGE_ENABLED_STATE); |
| + this.id = id; |
| + this.enabled = enabled; |
| + } |
| + } |
| + |
| + private static class DownloadFinishedEvent extends EngineEvent |
| + { |
| + private final String id; |
| + private final int responseCode; |
| + private final String response; |
| + private final HashMap<String, String> headers = new HashMap<>(); |
| + |
| + public DownloadFinishedEvent(final String id, |
| + final int responseCode, |
| + final String response, |
| + final Map<String, String> headers) |
| + { |
| + super(EngineEvent.EngineEventType.DOWNLOAD_FINISHED); |
| + this.id = id; |
| + this.responseCode = responseCode; |
| + this.response = response; |
| + if (headers != null) |
| + { |
| + this.headers.putAll(headers); |
| + } |
| + } |
| + } |
| + |
| + public void enqueueDownload(final Subscription sub, final boolean forced) throws IOException |
| + { |
| + if (sub.getURL() != null && sub.shouldUpdate(forced)) |
| + { |
| + final HashMap<String, String> headers = new HashMap<>(); |
| + if (sub.isMetaDataValid() && sub.isFiltersValid()) |
| + { |
| + final String lastModified = sub.getMeta(Subscription.KEY_HTTP_LAST_MODIFIED); |
| + if (!TextUtils.isEmpty(lastModified)) |
| + { |
| + headers.put("If-Modified-Since", lastModified); |
| + } |
| + final String etag = sub.getMeta(Subscription.KEY_HTTP_ETAG); |
| + if (!TextUtils.isEmpty(etag)) |
| + { |
| + headers.put("If-None-Match", etag); |
| + } |
| + } |
| + Log.d(TAG, headers.toString()); |
| + this.downloader.enqueueDownload(this.createDownloadURL(sub), sub.getId(), headers); |
| + } |
| + } |
| + |
| + public void connectivityChanged() |
| + { |
| + this.downloader.connectivityChanged(); |
| + } |
| + |
| + public interface SubscriptionUpdateCallback |
| + { |
| + void subscriptionUpdateRequested(boolean enabled); |
| + void subscriptionUpdatedApplied(); |
| + } |
| + |
| + public interface SubscriptionAddedCallback |
| + { |
| + void subscriptionAdded(); |
| + } |
| +} |