OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * This file is part of Adblock Plus <https://adblockplus.org/>, |
| 3 * Copyright (C) 2006-present eyeo GmbH |
| 4 * |
| 5 * Adblock Plus is free software: you can redistribute it and/or modify |
| 6 * it under the terms of the GNU General Public License version 3 as |
| 7 * published by the Free Software Foundation. |
| 8 * |
| 9 * Adblock Plus is distributed in the hope that it will be useful, |
| 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 * GNU General Public License for more details. |
| 13 * |
| 14 * You should have received a copy of the GNU General Public License |
| 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
| 16 */ |
| 17 |
| 18 package org.adblockplus.sbrowser.contentblocker.engine; |
| 19 |
| 20 import java.io.BufferedReader; |
| 21 import java.io.BufferedWriter; |
| 22 import java.io.File; |
| 23 import java.io.FileOutputStream; |
| 24 import java.io.IOException; |
| 25 import java.io.InputStream; |
| 26 import java.io.InputStreamReader; |
| 27 import java.io.OutputStreamWriter; |
| 28 import java.io.Writer; |
| 29 import java.net.URI; |
| 30 import java.net.URISyntaxException; |
| 31 import java.net.URL; |
| 32 import java.net.URLEncoder; |
| 33 import java.nio.charset.StandardCharsets; |
| 34 import java.util.ArrayList; |
| 35 import java.util.Collections; |
| 36 import java.util.HashMap; |
| 37 import java.util.List; |
| 38 import java.util.Map; |
| 39 import java.util.Set; |
| 40 import java.util.TreeSet; |
| 41 import java.util.concurrent.LinkedBlockingQueue; |
| 42 import java.util.concurrent.TimeUnit; |
| 43 import java.util.concurrent.locks.ReentrantLock; |
| 44 |
| 45 import org.adblockplus.adblockplussbrowser.R; |
| 46 import org.adblockplus.sbrowser.contentblocker.util.SharedPrefsUtils; |
| 47 import org.adblockplus.sbrowser.contentblocker.util.SubscriptionUtils; |
| 48 |
| 49 import android.content.Context; |
| 50 import android.content.Intent; |
| 51 import android.content.pm.PackageInfo; |
| 52 import android.content.pm.PackageManager; |
| 53 import android.content.pm.ResolveInfo; |
| 54 import android.net.ConnectivityManager; |
| 55 import android.net.NetworkInfo; |
| 56 import android.net.Uri; |
| 57 import android.os.Handler; |
| 58 import android.os.Looper; |
| 59 import android.support.annotation.VisibleForTesting; |
| 60 import android.text.TextUtils; |
| 61 import android.text.format.DateUtils; |
| 62 import android.util.Log; |
| 63 |
| 64 public final class Engine |
| 65 { |
| 66 private static final String TAG = Engine.class.getSimpleName(); |
| 67 |
| 68 public static final String USER_FILTERS_TITLE = "__filters"; |
| 69 public static final String USER_EXCEPTIONS_TITLE = "__exceptions"; |
| 70 |
| 71 public static final String SBROWSER_APP_ID = "com.sec.android.app.sbrowser"; |
| 72 public static final String EASYLIST_URL = "https://easylist-downloads.adblockp
lus.org/easylist.txt"; |
| 73 private static final String ACTION_OPEN_SETTINGS = "com.samsung.android.sbrows
er.contentBlocker.ACTION_SETTING"; |
| 74 private static final String ACTION_UPDATE = "com.samsung.android.sbrowser.cont
entBlocker.ACTION_UPDATE"; |
| 75 |
| 76 public static final String SUBSCRIPTIONS_EXCEPTIONSURL = "subscriptions_except
ionsurl"; |
| 77 |
| 78 // The value below specifies an interval of [x, 2*x[, where x = |
| 79 // INITIAL_UPDATE_CHECK_DELAY |
| 80 private static final long INITIAL_UPDATE_CHECK_DELAY = 5 * DateUtils.SECOND_IN
_MILLIS; |
| 81 private static final long UPDATE_CHECK_INTERVAL = 30 * DateUtils.MINUTE_IN_MIL
LIS; |
| 82 private static final long BROADCAST_COMBINATION_DELAY = 2500; |
| 83 |
| 84 private static final int NO_FLAG = 0; |
| 85 private static final int OLDEST_SAMSUNG_INTERNET_5_VERSIONCODE = 500000000; |
| 86 |
| 87 private final ReentrantLock accessLock = new ReentrantLock(); |
| 88 private DefaultSubscriptions defaultSubscriptions; |
| 89 private Subscriptions subscriptions; |
| 90 private JSONPrefs jsonPrefs; |
| 91 private AppInfo appInfo; |
| 92 private final LinkedBlockingQueue<EngineEvent> engineEvents = new LinkedBlocki
ngQueue<>(); |
| 93 private Thread handlerThread; |
| 94 private Downloader downloader; |
| 95 private SubscriptionUpdateCallback subscriptionUpdateCallback; |
| 96 private final Context serviceContext; |
| 97 private boolean wasFirstRun = false; |
| 98 private long nextUpdateBroadcast = Long.MAX_VALUE; |
| 99 |
| 100 private Engine(final Context context) |
| 101 { |
| 102 this.serviceContext = context; |
| 103 } |
| 104 |
| 105 public String getPrefsDefault(final String key) |
| 106 { |
| 107 return this.jsonPrefs.getDefaults(key); |
| 108 } |
| 109 |
| 110 DefaultSubscriptionInfo getDefaultSubscriptionInfo(final Subscription sub) |
| 111 { |
| 112 return this.defaultSubscriptions.getForUrl(sub.getURL()); |
| 113 } |
| 114 |
| 115 void lock() |
| 116 { |
| 117 this.accessLock.lock(); |
| 118 } |
| 119 |
| 120 void unlock() |
| 121 { |
| 122 this.accessLock.unlock(); |
| 123 } |
| 124 |
| 125 public static boolean openSBrowserSettings(final Context activityContext) |
| 126 { |
| 127 final Intent intent = new Intent(ACTION_OPEN_SETTINGS); |
| 128 final List<ResolveInfo> list = activityContext.getPackageManager() |
| 129 .queryIntentActivities(intent, 0); |
| 130 if (list.size() > 0) |
| 131 { |
| 132 activityContext.startActivity(intent); |
| 133 } |
| 134 return list.size() > 0; |
| 135 } |
| 136 |
| 137 public static boolean hasCompatibleSBrowserInstalled(final Context activityCon
text) |
| 138 { |
| 139 try |
| 140 { |
| 141 return activityContext.getPackageManager() |
| 142 .queryIntentActivities(new Intent(ACTION_OPEN_SETTINGS), 0).size() > 0
; |
| 143 } |
| 144 catch (final Throwable t) |
| 145 { |
| 146 return false; |
| 147 } |
| 148 } |
| 149 |
| 150 /** |
| 151 * Starting with Samsung Internet 5.0, the way to enable ad blocking has chang
ed. As a result, we |
| 152 * need to check for the version of Samsung Internet and apply text changes to
the first run slide. |
| 153 * |
| 154 * @param activityContext |
| 155 * @return a boolean that indicates, if the user has Samsung Internet version
5.x |
| 156 */ |
| 157 public static boolean hasSamsungInternetVersion5OrNewer(final Context activity
Context) |
| 158 { |
| 159 try |
| 160 { |
| 161 PackageInfo packageInfo = activityContext.getPackageManager().getPackageIn
fo(SBROWSER_APP_ID, NO_FLAG); |
| 162 return packageInfo.versionCode >= OLDEST_SAMSUNG_INTERNET_5_VERSIONCODE; |
| 163 } |
| 164 catch (PackageManager.NameNotFoundException e) |
| 165 { |
| 166 // Should never happen, as checkAAStatusAndProceed() should not be called
if the user |
| 167 // has no compatible SBrowser installed. Nevertheless we have to handle th
e Exception. |
| 168 Log.d(TAG, "No compatible Samsung Browser found.", e); |
| 169 return false; |
| 170 } |
| 171 } |
| 172 |
| 173 public void setSubscriptionUpdateCallback(final SubscriptionUpdateCallback sub
scriptionUpdateCallback) |
| 174 { |
| 175 this.subscriptionUpdateCallback = subscriptionUpdateCallback; |
| 176 } |
| 177 |
| 178 public void requestUpdateBroadcast() |
| 179 { |
| 180 this.nextUpdateBroadcast = System.currentTimeMillis() + BROADCAST_COMBINATIO
N_DELAY; |
| 181 } |
| 182 |
| 183 private void writeFileAndSendUpdateBroadcast() |
| 184 { |
| 185 createAndWriteFile(); |
| 186 |
| 187 runOnUiThread(new Runnable() |
| 188 { |
| 189 @Override |
| 190 public void run() |
| 191 { |
| 192 final Intent intent = new Intent(); |
| 193 intent.setAction(ACTION_UPDATE); |
| 194 intent.setData(Uri.parse("package:" + Engine.this.serviceContext.getPack
ageName())); |
| 195 Engine.this.serviceContext.sendBroadcast(intent); |
| 196 } |
| 197 }); |
| 198 } |
| 199 |
| 200 boolean canUseInternet() |
| 201 { |
| 202 final ConnectivityManager connManager = (ConnectivityManager) this.serviceCo
ntext |
| 203 .getSystemService(Context.CONNECTIVITY_SERVICE); |
| 204 final NetworkInfo current = connManager.getActiveNetworkInfo(); |
| 205 if (current == null) |
| 206 { |
| 207 return false; |
| 208 } |
| 209 |
| 210 if (wasFirstRun()) |
| 211 { |
| 212 return true; |
| 213 } |
| 214 |
| 215 final boolean wifiOnly = "1".equals(SharedPrefsUtils.getString( |
| 216 this.serviceContext, R.string.key_automatic_updates , "1")); |
| 217 |
| 218 if (wifiOnly) |
| 219 { |
| 220 if (current.isConnected() && !current.isRoaming()) |
| 221 { |
| 222 switch (current.getType()) |
| 223 { |
| 224 case ConnectivityManager.TYPE_BLUETOOTH: |
| 225 case ConnectivityManager.TYPE_ETHERNET: |
| 226 case ConnectivityManager.TYPE_WIFI: |
| 227 case ConnectivityManager.TYPE_WIMAX: |
| 228 return true; |
| 229 default: |
| 230 return false; |
| 231 } |
| 232 } |
| 233 return false; |
| 234 } |
| 235 return current.isConnected(); |
| 236 } |
| 237 |
| 238 public List<SubscriptionInfo> getListedSubscriptions() |
| 239 { |
| 240 return this.subscriptions.getSubscriptions(this); |
| 241 } |
| 242 |
| 243 public void changeSubscriptionState(final String id, final boolean enabled) |
| 244 { |
| 245 if (this.subscriptionUpdateCallback != null) |
| 246 { |
| 247 subscriptionUpdateCallback.subscriptionUpdateRequested(enabled); |
| 248 } |
| 249 this.engineEvents.add(new ChangeEnabledStateEvent(id, enabled)); |
| 250 } |
| 251 |
| 252 public void subscriptionStateChanged() |
| 253 { |
| 254 if (this.subscriptionUpdateCallback != null) |
| 255 { |
| 256 subscriptionUpdateCallback.subscriptionUpdatedApplied(); |
| 257 } |
| 258 } |
| 259 |
| 260 public void createAndAddSubscriptionFromUrl(final String url, |
| 261 final SubscriptionAddedCallback callback) throws IOException |
| 262 { |
| 263 final Subscription sub = Subscription.create(url); |
| 264 sub.putMeta(Subscription.KEY_TITLE, url); |
| 265 sub.setEnabled(true); |
| 266 subscriptions.add(sub); |
| 267 subscriptions.persistSubscription(sub); |
| 268 callback.subscriptionAdded(); |
| 269 } |
| 270 |
| 271 public void removeSubscriptionById(final String subscriptionId) |
| 272 { |
| 273 subscriptions.remove(subscriptionId); |
| 274 } |
| 275 |
| 276 void downloadFinished(final String id, final int responseCode, final String re
sponse, |
| 277 final Map<String, String> headers) |
| 278 { |
| 279 this.engineEvents.add(new DownloadFinishedEvent(id, responseCode, response,
headers)); |
| 280 } |
| 281 |
| 282 private void createAndWriteFile() |
| 283 { |
| 284 this.lock(); |
| 285 try |
| 286 { |
| 287 Log.d(TAG, "Writing filters..."); |
| 288 final File filterFile = this.subscriptions.createAndWriteFile(); |
| 289 writeWhitelistedWebsites(this.serviceContext, filterFile); |
| 290 |
| 291 SharedPrefsUtils.putString( |
| 292 this.serviceContext, R.string.key_cached_filter_path, filterFile.getAb
solutePath()); |
| 293 |
| 294 Log.d(TAG, "Cleaning up cache..."); |
| 295 final File dummyFile = getDummyFilterFile(this.serviceContext); |
| 296 final File[] cacheDirFiles = getFilterCacheDir(this.serviceContext).listFi
les(); |
| 297 if (cacheDirFiles != null) |
| 298 { |
| 299 for (final File file : cacheDirFiles) |
| 300 { |
| 301 if (!file.equals(dummyFile) && !file.equals(filterFile)) |
| 302 { |
| 303 Log.d(TAG, "Deleting file:" + file); |
| 304 file.delete(); |
| 305 } |
| 306 } |
| 307 } |
| 308 } |
| 309 catch (IOException e) |
| 310 { |
| 311 Log.e(TAG, "Failed to write filters", e); |
| 312 } |
| 313 finally |
| 314 { |
| 315 this.unlock(); |
| 316 } |
| 317 } |
| 318 |
| 319 public static void runOnUiThread(final Runnable runnable) |
| 320 { |
| 321 new Handler(Looper.getMainLooper()).post(runnable); |
| 322 } |
| 323 |
| 324 public boolean isAcceptableAdsEnabled() |
| 325 { |
| 326 this.lock(); |
| 327 try |
| 328 { |
| 329 return this.subscriptions.isSubscriptionEnabled("url:" |
| 330 + this.getPrefsDefault(SUBSCRIPTIONS_EXCEPTIONSURL)); |
| 331 } |
| 332 finally |
| 333 { |
| 334 this.unlock(); |
| 335 } |
| 336 } |
| 337 |
| 338 public DefaultSubscriptionInfo getDefaultSubscriptionInfoForUrl(final String u
rl) |
| 339 { |
| 340 return this.defaultSubscriptions.getForUrl(url); |
| 341 } |
| 342 |
| 343 /** |
| 344 * If the user starts the app for the first time, we force to update the subsc
ription which was |
| 345 * selected as the default, no matter if he has a WIFI connection or not. From
the second start |
| 346 * we only update when the user has a WIFI connection. |
| 347 * |
| 348 * @return a boolean that indicated if this is the first start of the app |
| 349 */ |
| 350 @VisibleForTesting |
| 351 private boolean wasFirstRun() |
| 352 { |
| 353 if (wasFirstRun) |
| 354 { |
| 355 this.wasFirstRun = false; |
| 356 return true; |
| 357 } |
| 358 else |
| 359 { |
| 360 return false; |
| 361 } |
| 362 } |
| 363 |
| 364 private void migrateFromPreviousVersion(final Context context) |
| 365 { |
| 366 try |
| 367 { |
| 368 final int versionCode = context.getPackageManager().getPackageInfo(context
.getPackageName(), |
| 369 0).versionCode; |
| 370 |
| 371 final int previousVersionCode = SharedPrefsUtils.getInt( |
| 372 context, R.string.key_previous_version_code, 0); |
| 373 |
| 374 if (versionCode > previousVersionCode) |
| 375 { |
| 376 if (previousVersionCode > 0) |
| 377 { |
| 378 // We can do possible migration stuff here |
| 379 // Currently we only persist the new version code |
| 380 } |
| 381 SharedPrefsUtils.putInt(context, R.string.key_previous_version_code, ver
sionCode); |
| 382 } |
| 383 } |
| 384 catch (final Throwable t) |
| 385 { |
| 386 Log.e(TAG, "Failed on migration, please clear all application data", t); |
| 387 } |
| 388 } |
| 389 |
| 390 static Engine create(final Context context) throws IOException |
| 391 { |
| 392 final Engine engine = new Engine(context); |
| 393 |
| 394 // Migration data from previous version (if needed) |
| 395 engine.migrateFromPreviousVersion(context); |
| 396 Log.d(TAG, "Migration done"); |
| 397 |
| 398 engine.appInfo = AppInfo.create(context); |
| 399 |
| 400 Log.d(TAG, "Creating engine, appInfo=" + engine.appInfo.toString()); |
| 401 |
| 402 try (final InputStream subscriptionsXml = context.getResources() |
| 403 .openRawResource(R.raw.subscriptions)) |
| 404 { |
| 405 engine.defaultSubscriptions = DefaultSubscriptions.fromStream(subscription
sXml); |
| 406 } |
| 407 |
| 408 Log.d(TAG, "Finished reading 'subscriptions.xml'"); |
| 409 engine.subscriptions = Subscriptions.initialize(engine, getSubscriptionsDir(
context), |
| 410 getFilterCacheDir(context)); |
| 411 |
| 412 try (final InputStream prefsJson = context.getResources().openRawResource(R.
raw.prefs)) |
| 413 { |
| 414 engine.jsonPrefs = JSONPrefs.create(prefsJson); |
| 415 } |
| 416 |
| 417 Log.d(TAG, "Finished reading JSON preferences"); |
| 418 |
| 419 // Check if this is a fresh start, if so: initialize bundled easylist. |
| 420 engine.wasFirstRun = engine.subscriptions.wasUnitialized(); |
| 421 if (engine.subscriptions.wasUnitialized()) |
| 422 { |
| 423 Log.d(TAG, "Subscription storage was uninitialized, initializing..."); |
| 424 |
| 425 try (final InputStream easylistTxt = context.getResources().openRawResourc
e(R.raw.easylist)) |
| 426 { |
| 427 final Subscription easylist = engine.subscriptions.add(Subscription |
| 428 // Use bundled EasyList as default and update it with locale specifi
c list later |
| 429 // see: https://issues.adblockplus.org/ticket/5237 |
| 430 .create(SubscriptionUtils.chooseDefaultSubscriptionUrl( |
| 431 engine.defaultSubscriptions.getAdsSubscriptions())) |
| 432 .parseLines(readLines(easylistTxt))); |
| 433 easylist.putMeta(Subscription.KEY_UPDATE_TIMESTAMP, "0"); |
| 434 easylist.setEnabled(true); |
| 435 } |
| 436 Log.d(TAG, "Added and enabled bundled easylist"); |
| 437 |
| 438 try (final InputStream exceptionsTxt = context.getResources() |
| 439 .openRawResource(R.raw.exceptionrules)) |
| 440 { |
| 441 final Subscription exceptions = engine.subscriptions.add(Subscription |
| 442 .create(engine.getPrefsDefault(SUBSCRIPTIONS_EXCEPTIONSURL)) |
| 443 .parseLines(readLines(exceptionsTxt))); |
| 444 exceptions.putMeta(Subscription.KEY_UPDATE_TIMESTAMP, "0"); |
| 445 exceptions.setEnabled(true); |
| 446 } |
| 447 Log.d(TAG, "Added and enabled bundled exceptionslist"); |
| 448 |
| 449 int additional = 0; |
| 450 for (final Subscription sub : engine.defaultSubscriptions.createSubscripti
ons()) |
| 451 { |
| 452 if (!engine.subscriptions.hasSubscription(sub.getId())) |
| 453 { |
| 454 additional++; |
| 455 engine.subscriptions.add(sub); |
| 456 } |
| 457 } |
| 458 |
| 459 Log.d(TAG, "Added " + additional + " additional default/built-in subscript
ions"); |
| 460 engine.subscriptions.persistSubscriptions(); |
| 461 } |
| 462 |
| 463 engine.handlerThread = new Thread(new EventHandler(engine)); |
| 464 engine.handlerThread.setDaemon(true); |
| 465 engine.handlerThread.start(); |
| 466 |
| 467 engine.downloader = Downloader.create(engine); |
| 468 |
| 469 final File cachedFilterFile = getCachedFilterFile(context); |
| 470 if (cachedFilterFile == null || !cachedFilterFile.exists()) |
| 471 { |
| 472 engine.writeFileAndSendUpdateBroadcast(); |
| 473 } |
| 474 |
| 475 return engine; |
| 476 } |
| 477 |
| 478 public static String readFileAsString(InputStream instream) throws IOException |
| 479 { |
| 480 final StringBuilder sb = new StringBuilder(); |
| 481 try (final BufferedReader r = new BufferedReader(new InputStreamReader( |
| 482 instream, StandardCharsets.UTF_8))) |
| 483 { |
| 484 for (int ch = r.read(); ch != -1; ch = r.read()) |
| 485 { |
| 486 sb.append((char) ch); |
| 487 } |
| 488 } |
| 489 return sb.toString(); |
| 490 } |
| 491 |
| 492 public static List<String> readLines(InputStream instream) throws IOException |
| 493 { |
| 494 final ArrayList<String> list = new ArrayList<>(); |
| 495 try (final BufferedReader r = new BufferedReader(new InputStreamReader( |
| 496 instream, StandardCharsets.UTF_8))) |
| 497 { |
| 498 for (String line = r.readLine(); line != null; line = r.readLine()) |
| 499 { |
| 500 list.add(line); |
| 501 } |
| 502 } |
| 503 return list; |
| 504 } |
| 505 |
| 506 public static File getOrCreateCachedFilterFile(Context context) throws IOExcep
tion |
| 507 { |
| 508 final File cachedFilterFile = getCachedFilterFile(context); |
| 509 if (cachedFilterFile != null && cachedFilterFile.exists()) |
| 510 { |
| 511 Log.d(TAG, "Cached filter file found: " + cachedFilterFile); |
| 512 return cachedFilterFile; |
| 513 } |
| 514 |
| 515 Log.d(TAG, "Cached filter file not found. Using dummy filter file"); |
| 516 final File dummyFilterFile = getDummyFilterFile(context); |
| 517 if (!dummyFilterFile.exists()) |
| 518 { |
| 519 Log.d(TAG, "Creating dummy filter file..."); |
| 520 dummyFilterFile.getParentFile().mkdirs(); |
| 521 try (final BufferedWriter writer = new BufferedWriter(new OutputStreamWrit
er( |
| 522 new FileOutputStream(dummyFilterFile), StandardCharsets.UTF_8))) |
| 523 { |
| 524 writeFilterHeaders(writer); |
| 525 } |
| 526 } |
| 527 return dummyFilterFile; |
| 528 } |
| 529 |
| 530 public static void writeFilterHeaders(Writer writer) throws IOException |
| 531 { |
| 532 writer.write("[Adblock Plus 2.0]\n"); |
| 533 writer.write("! This file was automatically created.\n"); |
| 534 } |
| 535 |
| 536 private static void writeWhitelistedWebsites(Context context, File filterFile)
throws IOException |
| 537 { |
| 538 Log.d(TAG, "Writing whitelisted websites..."); |
| 539 final Set<String> whitelistedWebsites = new TreeSet<>(); |
| 540 whitelistedWebsites.addAll(SharedPrefsUtils.getStringSet( |
| 541 context, R.string.key_whitelisted_websites, Collections.<String>emptySet
())); |
| 542 |
| 543 try (final BufferedWriter w = new BufferedWriter( new OutputStreamWriter( |
| 544 new FileOutputStream(filterFile, true), StandardCharsets.UTF_8))) |
| 545 { |
| 546 for (final String url : whitelistedWebsites) |
| 547 { |
| 548 try |
| 549 { |
| 550 final URI uri = new URI(url); |
| 551 final String host = uri.getHost() != null ? uri.getHost() : uri.getPat
h(); |
| 552 w.write("@@||" + host + "^$document"); |
| 553 w.write('\n'); |
| 554 } |
| 555 catch (URISyntaxException e) |
| 556 { |
| 557 Log.w(TAG, "Failed to parse whitelisted website: " + url); |
| 558 } |
| 559 } |
| 560 } |
| 561 } |
| 562 |
| 563 private static File getCachedFilterFile(Context context) |
| 564 { |
| 565 final String cachedFilterPath = SharedPrefsUtils.getString( |
| 566 context, R.string.key_cached_filter_path, null); |
| 567 |
| 568 if (cachedFilterPath != null) |
| 569 { |
| 570 return new File(cachedFilterPath); |
| 571 } |
| 572 |
| 573 return null; |
| 574 } |
| 575 |
| 576 private static File getDummyFilterFile(Context context) |
| 577 { |
| 578 return new File(getFilterCacheDir(context), "dummy.txt"); |
| 579 } |
| 580 |
| 581 private static File getFilterCacheDir(Context context) |
| 582 { |
| 583 return new File(context.getCacheDir(), "subscriptions"); |
| 584 } |
| 585 |
| 586 private static File getSubscriptionsDir(Context context) |
| 587 { |
| 588 return new File(context.getFilesDir(), "subscriptions"); |
| 589 } |
| 590 |
| 591 URL createDownloadURL(final Subscription sub) throws IOException |
| 592 { |
| 593 final StringBuilder sb = new StringBuilder(); |
| 594 |
| 595 sb.append(sub.getURL()); |
| 596 if (sub.getURL().getQuery() != null) |
| 597 { |
| 598 sb.append('&'); |
| 599 } |
| 600 else |
| 601 { |
| 602 sb.append('?'); |
| 603 } |
| 604 |
| 605 sb.append("addonName="); |
| 606 sb.append(URLEncoder.encode(this.appInfo.addonName, StandardCharsets.UTF_8.n
ame())); |
| 607 sb.append("&addonVersion="); |
| 608 sb.append(URLEncoder.encode(this.appInfo.addonVersion, StandardCharsets.UTF_
8.name())); |
| 609 sb.append("&application="); |
| 610 sb.append(URLEncoder.encode(this.appInfo.application, StandardCharsets.UTF_8
.name())); |
| 611 sb.append("&applicationVersion="); |
| 612 sb.append(URLEncoder.encode(this.appInfo.applicationVersion, StandardCharset
s.UTF_8.name())); |
| 613 sb.append("&platform="); |
| 614 sb.append(URLEncoder.encode(this.appInfo.platform, StandardCharsets.UTF_8.na
me())); |
| 615 sb.append("&platformVersion="); |
| 616 sb.append(URLEncoder.encode(this.appInfo.platformVersion, StandardCharsets.U
TF_8.name())); |
| 617 sb.append("&lastVersion="); |
| 618 sb.append(sub.getVersion()); |
| 619 sb.append("&downloadCount="); |
| 620 final long downloadCount = sub.getDownloadCount(); |
| 621 if (downloadCount < 5) |
| 622 { |
| 623 sb.append(downloadCount); |
| 624 } |
| 625 else |
| 626 { |
| 627 sb.append("4%2B"); // "4+" URL encoded |
| 628 } |
| 629 |
| 630 return new URL(sb.toString()); |
| 631 } |
| 632 |
| 633 public boolean isAcceptableAdsUrl(final SubscriptionInfo subscriptionInfo) |
| 634 { |
| 635 return getPrefsDefault(SUBSCRIPTIONS_EXCEPTIONSURL).equals(subscriptionInfo.
getUrl()); |
| 636 } |
| 637 |
| 638 private static class EventHandler implements Runnable |
| 639 { |
| 640 private static final String TAG = EventHandler.class.getSimpleName(); |
| 641 private final Engine engine; |
| 642 |
| 643 public EventHandler(final Engine engine) |
| 644 { |
| 645 this.engine = engine; |
| 646 } |
| 647 |
| 648 @Override |
| 649 public void run() |
| 650 { |
| 651 Log.d(TAG, "Handler thread started"); |
| 652 boolean interrupted = false; |
| 653 long nextUpdateCheck = System.currentTimeMillis() |
| 654 + (long) ((1 + Math.random()) * INITIAL_UPDATE_CHECK_DELAY); |
| 655 while (!interrupted) |
| 656 { |
| 657 try |
| 658 { |
| 659 final EngineEvent event = this.engine.engineEvents.poll(100, TimeUnit.
MILLISECONDS); |
| 660 engine.lock(); |
| 661 try |
| 662 { |
| 663 if (event != null) |
| 664 { |
| 665 switch (event.getType()) |
| 666 { |
| 667 case CHANGE_ENABLED_STATE: |
| 668 { |
| 669 final ChangeEnabledStateEvent cese = (ChangeEnabledStateEvent)
event; |
| 670 Log.d(TAG, "Changing " + cese.id + " to enabled: " + cese.enab
led); |
| 671 engine.subscriptions.changeSubscriptionState(cese.id, cese.ena
bled); |
| 672 break; |
| 673 } |
| 674 case DOWNLOAD_FINISHED: |
| 675 { |
| 676 final DownloadFinishedEvent dfe = (DownloadFinishedEvent) even
t; |
| 677 Log.d(TAG, "Download finished for '" + dfe.id + "' with respon
se code " |
| 678 + dfe.responseCode); |
| 679 this.engine.subscriptions.updateSubscription(dfe.id, dfe.respo
nseCode, |
| 680 dfe.response, dfe.headers); |
| 681 break; |
| 682 } |
| 683 default: |
| 684 Log.d(TAG, "Unhandled type: " + event.getType()); |
| 685 break; |
| 686 } |
| 687 } |
| 688 |
| 689 final long currentTime = System.currentTimeMillis(); |
| 690 if (currentTime > nextUpdateCheck) |
| 691 { |
| 692 nextUpdateCheck = currentTime + UPDATE_CHECK_INTERVAL; |
| 693 |
| 694 this.engine.subscriptions.checkForUpdates(); |
| 695 } |
| 696 |
| 697 if (currentTime > this.engine.nextUpdateBroadcast) |
| 698 { |
| 699 this.engine.nextUpdateBroadcast = Long.MAX_VALUE; |
| 700 Log.d(TAG, "Sending update broadcast"); |
| 701 this.engine.writeFileAndSendUpdateBroadcast(); |
| 702 } |
| 703 } |
| 704 finally |
| 705 { |
| 706 engine.unlock(); |
| 707 } |
| 708 } |
| 709 catch (final InterruptedException e) |
| 710 { |
| 711 Log.d(TAG, "Handler interrupted", e); |
| 712 interrupted = true; |
| 713 } |
| 714 catch (final Throwable t) |
| 715 { |
| 716 Log.e(TAG, "Event processing failed: " + t.getMessage(), t); |
| 717 } |
| 718 } |
| 719 Log.d(TAG, "Handler thread finished"); |
| 720 } |
| 721 } |
| 722 |
| 723 private static class EngineEvent |
| 724 { |
| 725 public enum EngineEventType |
| 726 { |
| 727 CHANGE_ENABLED_STATE, |
| 728 FORCE_DOWNLOAD, |
| 729 DOWNLOAD_FINISHED |
| 730 } |
| 731 |
| 732 private final EngineEventType type; |
| 733 |
| 734 EngineEvent(final EngineEventType type) |
| 735 { |
| 736 this.type = type; |
| 737 } |
| 738 |
| 739 public EngineEventType getType() |
| 740 { |
| 741 return this.type; |
| 742 } |
| 743 } |
| 744 |
| 745 private static class ChangeEnabledStateEvent extends EngineEvent |
| 746 { |
| 747 private final String id; |
| 748 private final boolean enabled; |
| 749 |
| 750 public ChangeEnabledStateEvent(final String id, final boolean enabled) |
| 751 { |
| 752 super(EngineEvent.EngineEventType.CHANGE_ENABLED_STATE); |
| 753 this.id = id; |
| 754 this.enabled = enabled; |
| 755 } |
| 756 } |
| 757 |
| 758 private static class DownloadFinishedEvent extends EngineEvent |
| 759 { |
| 760 private final String id; |
| 761 private final int responseCode; |
| 762 private final String response; |
| 763 private final HashMap<String, String> headers = new HashMap<>(); |
| 764 |
| 765 public DownloadFinishedEvent(final String id, |
| 766 final int responseCode, |
| 767 final String response, |
| 768 final Map<String, String> headers) |
| 769 { |
| 770 super(EngineEvent.EngineEventType.DOWNLOAD_FINISHED); |
| 771 this.id = id; |
| 772 this.responseCode = responseCode; |
| 773 this.response = response; |
| 774 if (headers != null) |
| 775 { |
| 776 this.headers.putAll(headers); |
| 777 } |
| 778 } |
| 779 } |
| 780 |
| 781 public void enqueueDownload(final Subscription sub, final boolean forced) thro
ws IOException |
| 782 { |
| 783 if (sub.getURL() != null && sub.shouldUpdate(forced)) |
| 784 { |
| 785 final HashMap<String, String> headers = new HashMap<>(); |
| 786 if (sub.isMetaDataValid() && sub.isFiltersValid()) |
| 787 { |
| 788 final String lastModified = sub.getMeta(Subscription.KEY_HTTP_LAST_MODIF
IED); |
| 789 if (!TextUtils.isEmpty(lastModified)) |
| 790 { |
| 791 headers.put("If-Modified-Since", lastModified); |
| 792 } |
| 793 final String etag = sub.getMeta(Subscription.KEY_HTTP_ETAG); |
| 794 if (!TextUtils.isEmpty(etag)) |
| 795 { |
| 796 headers.put("If-None-Match", etag); |
| 797 } |
| 798 } |
| 799 Log.d(TAG, headers.toString()); |
| 800 this.downloader.enqueueDownload(this.createDownloadURL(sub), sub.getId(),
headers); |
| 801 } |
| 802 } |
| 803 |
| 804 public void connectivityChanged() |
| 805 { |
| 806 this.downloader.connectivityChanged(); |
| 807 } |
| 808 |
| 809 public interface SubscriptionUpdateCallback |
| 810 { |
| 811 void subscriptionUpdateRequested(boolean enabled); |
| 812 void subscriptionUpdatedApplied(); |
| 813 } |
| 814 |
| 815 public interface SubscriptionAddedCallback |
| 816 { |
| 817 void subscriptionAdded(); |
| 818 } |
| 819 } |
OLD | NEW |