Index: libadblockplus-android-tests/src/org/adblockplus/libadblockplus/MockFilterChangeCallback.java
diff --git a/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/MockFilterChangeCallback.java b/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/MockFilterChangeCallback.java
index a300d7c1aa4856fb101b928bef62d20967349fad..974b54da2addaa5e4d8f965ed73a91ca82cecbe3 100644
--- a/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/MockFilterChangeCallback.java
+++ b/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/MockFilterChangeCallback.java
@@ -17,9 +17,15 @@
 
 package org.adblockplus.libadblockplus;
 
+import android.util.Log;
+
+import org.adblockplus.libadblockplus.android.Utils;
+
 public class MockFilterChangeCallback extends FilterChangeCallback
 {
-  private int timesCalled;
+  private static final String TAG = Utils.getTag(MockFilterChangeCallback.class);
+
+  private volatile int timesCalled;
 
   public MockFilterChangeCallback(int timesCalled)
   {
@@ -34,6 +40,7 @@ public class MockFilterChangeCallback extends FilterChangeCallback
   @Override
   public void filterChangeCallback(String action, JsValue jsValue)
   {
+    Log.d(TAG, "callback: action=" + action + ", jsValue=" + jsValue);
     timesCalled++;
   }
 }
Index: libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/AndroidFileSystemTest.java
diff --git a/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/AndroidFileSystemTest.java b/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/AndroidFileSystemTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8b693ac8f59709c320ebf5ea8851063d018f2548
--- /dev/null
+++ b/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/AndroidFileSystemTest.java
@@ -0,0 +1,432 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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.libadblockplus.tests;
+
+import android.os.SystemClock;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import org.adblockplus.libadblockplus.AdblockPlusException;
+import org.adblockplus.libadblockplus.AppInfo;
+import org.adblockplus.libadblockplus.FileSystem;
+import org.adblockplus.libadblockplus.FileSystemUtils;
+import org.adblockplus.libadblockplus.FilterEngine;
+import org.adblockplus.libadblockplus.JsEngine;
+import org.adblockplus.libadblockplus.android.AndroidFileSystem;
+import org.adblockplus.libadblockplus.android.AndroidWebRequest;
+import org.adblockplus.libadblockplus.android.Utils;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.adblockplus.libadblockplus.FileSystemUtils.byteFromInt;
+
+public class AndroidFileSystemTest extends AndroidTestCase
+{
+  private static final String TAG = Utils.getTag(AndroidFileSystemTest.class);
+
+  protected static final String TEXT_DATA = "12345qwerty";
+  protected static final byte[] EMPTY_DATA_BYTES = {};
+  protected static final byte[] TEXT_DATA_BYTES = TEXT_DATA.getBytes();
+  protected static final byte[] BINARY_DATA_BYTES = new byte[]
+  {
+    byteFromInt(1),
+    byteFromInt(2),
+    byteFromInt(3)
+  };
+
+  protected final FileSystemUtils fileSystemUtils = new FileSystemUtils();
+  protected File basePathFile;
+  protected AndroidFileSystem fileSystem;
+
+  @Override
+  protected void setUp() throws Exception
+  {
+    String tmpFolder = FileSystemUtils.generateUniqueFileName("tmp", null);
+    basePathFile = new File(getContext().getCacheDir(), tmpFolder);
+    basePathFile.mkdirs();
+
+    fileSystem = new AndroidFileSystem(basePathFile);
+  }
+
+  protected String generateUniqueFilename()
+  {
+    return fileSystemUtils.generateUniqueFileName("file", ".tmp");
+  }
+
+  protected File getFullPath(String path)
+  {
+    return new File(fileSystem.getBasePath(), path);
+  }
+
+  @Test
+  public void testWriteReadBinaryFile() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    fileSystem.write(resolvedPath, BINARY_DATA_BYTES);
+
+    assertTrue(file.exists());
+    assertTrue(Arrays.equals(BINARY_DATA_BYTES, fileSystemUtils.readFile(file)));
+
+    byte[] data = fileSystem.read(resolvedPath);
+    assertNotNull(data);
+    assertTrue(Arrays.equals(BINARY_DATA_BYTES, data));
+  }
+
+  @Test
+  public void testWriteNullFile() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    fileSystem.write(resolvedPath, null);
+
+    assertTrue(file.exists());
+    assertTrue(Arrays.equals(EMPTY_DATA_BYTES, fileSystemUtils.readFile(file)));
+  }
+
+  @Test
+  public void testWriteEmptyFile() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    fileSystem.write(resolvedPath, EMPTY_DATA_BYTES);
+
+    assertTrue(file.exists());
+    assertTrue(Arrays.equals(EMPTY_DATA_BYTES, fileSystemUtils.readFile(file)));
+  }
+
+  @Test
+  public void testWriteInvalidPath()
+  {
+    String path = "notExistingDirectory/" + generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+    File notExistingDirectoryFile = file.getParentFile();
+    assertFalse(notExistingDirectoryFile.exists());
+
+    try
+    {
+      fileSystem.write(resolvedPath, TEXT_DATA_BYTES);
+      fail("Exception should be thrown");
+    }
+    catch (AdblockPlusException e)
+    {
+      // ignored
+    }
+  }
+
+  @Test
+  public void testWriteSingleLineData() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    final String singleLineData = TEXT_DATA.replace("\n", "");
+    fileSystem.write(resolvedPath, singleLineData.getBytes());
+
+    assertTrue(file.exists());
+    assertTrue(Arrays.equals(singleLineData.getBytes(), fileSystemUtils.readFile(file)));
+  }
+
+  @Test
+  public void testWriteMultiLineData() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    final String multiLineData = TEXT_DATA + "\n" + TEXT_DATA;
+    fileSystem.write(resolvedPath, multiLineData.getBytes());
+
+    assertTrue(file.exists());
+    assertTrue(Arrays.equals(multiLineData.getBytes(), fileSystemUtils.readFile(file)));
+  }
+
+  @Test
+  public void testReadEmptyFile() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    fileSystemUtils.writeFile(file, EMPTY_DATA_BYTES);
+    assertTrue(file.exists());
+
+    assertTrue(Arrays.equals(EMPTY_DATA_BYTES, fileSystem.read(resolvedPath)));
+  }
+
+  @Test
+  public void testReadSingleLineData() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    final String singleLineData = TEXT_DATA.replace("\n", "");
+    fileSystemUtils.writeFile(file, singleLineData.getBytes());
+    assertTrue(file.exists());
+
+    assertTrue(Arrays.equals(singleLineData.getBytes(), fileSystem.read(resolvedPath)));
+  }
+
+  @Test
+  public void testReadMultiLineData() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    final String multiLineData = TEXT_DATA + "\n" + TEXT_DATA;
+    fileSystemUtils.writeFile(file, multiLineData.getBytes());
+    assertTrue(file.exists());
+
+    assertTrue(Arrays.equals(multiLineData.getBytes(), fileSystem.read(resolvedPath)));
+  }
+
+  @Test
+  public void testMoveExistingFile() throws IOException
+  {
+    String fromPath = generateUniqueFilename();
+    String resolvedFromPath= fileSystem.resolve(fromPath);
+    File fromFile = getFullPath(fromPath);
+    String toPath = generateUniqueFilename();
+    String resolvedToPath = fileSystem.resolve(toPath);
+    File toFile = getFullPath(toPath);
+    fileSystemUtils.writeFile(fromFile, TEXT_DATA_BYTES);
+
+    assertTrue(fromFile.exists());
+    assertFalse(toFile.exists());
+
+    fileSystem.move(resolvedFromPath, resolvedToPath);
+
+    assertFalse(fromFile.exists());
+    assertTrue(toFile.exists());
+    assertTrue(Arrays.equals(TEXT_DATA_BYTES, fileSystemUtils.readFile(toFile)));
+  }
+
+  @Test
+  public void testMoveNotExistingFile()
+  {
+    String fromPath = generateUniqueFilename();
+    String resolvedFromPath = fileSystem.resolve(fromPath);
+    File fromFile = getFullPath(fromPath);
+    String toPath = generateUniqueFilename();
+    String resolvedToPath = fileSystem.resolve(toPath);
+    assertFalse(fromFile.exists());
+
+    try
+    {
+      fileSystem.move(resolvedFromPath, resolvedToPath);
+      fail("Exception should be thrown");
+    }
+    catch (AdblockPlusException e)
+    {
+      // ignored
+    }
+  }
+
+  @Test
+  public void testRemoveExistingFile() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    fileSystemUtils.writeFile(file, TEXT_DATA_BYTES);
+
+    assertTrue(file.exists());
+    fileSystem.remove(resolvedPath);
+    assertFalse(file.exists());
+  }
+
+  @Test
+  public void testRemoveNotExistingFile()
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    fileSystem.remove(resolvedPath);
+    assertFalse(file.exists());
+  }
+
+  @Test
+  public void testStatExistingFile() throws IOException
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    fileSystemUtils.writeFile(file, TEXT_DATA_BYTES);
+    assertTrue(file.exists());
+
+    FileSystem.StatResult stat = fileSystem.stat(resolvedPath);
+    assertNotNull(stat);
+    assertTrue(stat.exists());
+    assertFalse(stat.isDirectory());
+    assertTrue(stat.isFile());
+    assertTrue(stat.getLastModified() > 0);
+  }
+
+  @Test
+  public void testStatExistingDirectory()
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    file.mkdir();
+
+    FileSystem.StatResult stat = fileSystem.stat(resolvedPath);
+    assertNotNull(stat);
+    assertTrue(stat.exists());
+    assertFalse(stat.isFile());
+    assertTrue(stat.isDirectory());
+    assertTrue(stat.getLastModified() > 0);
+  }
+
+  @Test
+  public void testStatNotExistingFile()
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    File file = getFullPath(path);
+    assertFalse(file.exists());
+
+    fileSystem.remove(resolvedPath);
+    assertFalse(file.exists());
+    FileSystem.StatResult notExistingStat = fileSystem.stat(resolvedPath);
+    assertFalse(notExistingStat.exists());
+    assertEquals(0l, notExistingStat.getLastModified());
+  }
+
+  @Test
+  public void testResolveAbsolute()
+  {
+    String path = generateUniqueFilename();
+    String resolvedPath = fileSystem.resolve(path);
+    String fullPath = getFullPath(path).getAbsolutePath();
+    assertEquals(fullPath, resolvedPath);
+  }
+
+  @Test
+  public void testResolveRelative()
+  {
+    String relativePath = "./folder/file2";
+    String resolvedPath = fileSystem.resolve(relativePath);
+    assertNotNull(resolvedPath);
+    assertEquals(new File(basePathFile, relativePath).getAbsolutePath(), resolvedPath);
+  }
+
+  private boolean readInvoked;
+  private boolean writeInvoked;
+  private boolean statInvoked;
+  private boolean resolveInvoked;
+
+  private final class TestAndroidFileSystem extends AndroidFileSystem
+  {
+    public TestAndroidFileSystem(File basePath)
+    {
+      super(basePath);
+    }
+
+    @Override
+    public byte[] read(String path)
+    {
+      readInvoked = true;
+      try
+      {
+        Log.d(TAG, "Reading from " + path);
+        String content = new String(FileSystemUtils.readFile(new File(path)), "UTF-8");
+        Log.d(TAG, "Read from " + path + ":\n" + content);
+      }
+      catch (IOException e)
+      {
+        throw new RuntimeException(e);
+      }
+
+      return super.read(path);
+    }
+
+    @Override
+    public void write(String path, byte[] data)
+    {
+      writeInvoked = true;
+      Log.d(TAG, "Write " + data.length + " bytes to " + path);
+      super.write(path, data);
+    }
+
+    @Override
+    public StatResult stat(String path)
+    {
+      statInvoked = true;
+      StatResult stat = super.stat(path);
+      Log.d(TAG, "Stat for " + path + " is " + stat);
+      return stat;
+    }
+
+    @Override
+    public String resolve(String path)
+    {
+      resolveInvoked = true;
+
+      String resolvedPath = super.resolve(path);
+      Log.d(TAG, "Resolve " + path + " to " + resolvedPath);
+      return resolvedPath;
+    }
+  }
+
+  @Test
+  public void testInvokeFromNativeCode()
+  {
+    readInvoked = false;
+    writeInvoked = false;
+    statInvoked = false;
+    resolveInvoked = false;
+
+    JsEngine jsEngine = new JsEngine(AppInfo.builder().build());
+    jsEngine.setDefaultLogSystem();
+    jsEngine.setFileSystem(new TestAndroidFileSystem(basePathFile));
+    jsEngine.setWebRequest(new AndroidWebRequest());
+
+    FilterEngine filterEngine = new FilterEngine(jsEngine);
+    SystemClock.sleep(10 * 1000);
+
+    assertTrue(readInvoked);
+    assertTrue(writeInvoked);
+    assertTrue(statInvoked);
+    assertTrue(resolveInvoked);
+  }
+}
Index: libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineGenericTest.java
diff --git a/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineGenericTest.java b/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineGenericTest.java
index 92a098f916017356fff71803647749eab2eefed4..242e1a94ab958405663a7a2aa9f61d125ce1ae4b 100644
--- a/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineGenericTest.java
+++ b/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineGenericTest.java
@@ -17,6 +17,7 @@
 
 package org.adblockplus.libadblockplus.tests;
 
+import org.adblockplus.libadblockplus.FileSystem;
 import org.adblockplus.libadblockplus.FilterEngine;
 import org.adblockplus.libadblockplus.LazyWebRequest;
 
@@ -24,6 +25,74 @@ public abstract class FilterEngineGenericTest extends BaseJsTest
 {
   protected FilterEngine filterEngine;
 
+  private static final class PatternsIniStubFileSystem extends FileSystem
+  {
+    private static final String PATTERNS_INI = "patterns.ini";
+
+    private boolean patternsIniExists = true;
+
+    public boolean isPatternsIniExists()
+    {
+      return patternsIniExists;
+    }
+
+    public void setPatternsIniExists(boolean patternsIniExists)
+    {
+      this.patternsIniExists = patternsIniExists;
+    }
+
+    @Override
+    public byte[] read(String path)
+    {
+      String result;
+      if (path.equals(PATTERNS_INI))
+      {
+        result = "# Adblock Plus preferences\n[Subscription]\nurl=~fl~";
+      }
+      else if (path.equals("prefs.json"))
+      {
+        result = "{}";
+      }
+      else
+      {
+        result = "";
+      }
+      return result.getBytes();
+    }
+
+    @Override
+    public void write(String path, byte[] data)
+    {
+
+    }
+
+    @Override
+    public void move(String fromPath, String toPath)
+    {
+
+    }
+
+    @Override
+    public void remove(String path)
+    {
+
+    }
+
+    @Override
+    public FileSystem.StatResult stat(String path)
+    {
+      return path.equals(PATTERNS_INI)
+        ? new FileSystem.StatResult(patternsIniExists, false, true, 0)
+        : new FileSystem.StatResult(false, false, false, 0);
+    }
+
+    @Override
+    public String resolve(String path)
+    {
+      return path;
+    }
+  }
+
   @Override
   protected void setUp() throws Exception
   {
@@ -31,6 +100,7 @@ public abstract class FilterEngineGenericTest extends BaseJsTest
 
     jsEngine.setWebRequest(new LazyWebRequest());
     jsEngine.setDefaultLogSystem();
+    jsEngine.setFileSystem(new PatternsIniStubFileSystem());
 
     filterEngine = new FilterEngine(jsEngine);
   }
Index: libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineTest.java
diff --git a/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineTest.java b/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineTest.java
index 40d1a24c4e9e12b22c3aeb58a9a515a2b00c482c..644be3cee52ae24266cae27d8df2570def4ea14a 100644
--- a/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineTest.java
+++ b/libadblockplus-android-tests/src/org/adblockplus/libadblockplus/tests/FilterEngineTest.java
@@ -17,6 +17,7 @@
 
 package org.adblockplus.libadblockplus.tests;
 
+import android.os.SystemClock;
 import android.util.Log;
 import org.adblockplus.libadblockplus.Filter;
 import org.adblockplus.libadblockplus.FilterEngine;
@@ -331,6 +332,12 @@ public class FilterEngineTest extends FilterEngineGenericTest
   @Test
   public void testSetRemoveFilterChangeCallback()
   {
+    // avoid callback with action "load"
+    while (filterEngine.getListedFilters().size() > 0)
+    {
+      filterEngine.getListedFilters().get(0).removeFromList();
+    }
+
     MockFilterChangeCallback mockFilterChangeCallback = new MockFilterChangeCallback(0);
 
     filterEngine.setFilterChangeCallback(mockFilterChangeCallback);
Index: libadblockplus-android/jni/Android.mk
diff --git a/libadblockplus-android/jni/Android.mk b/libadblockplus-android/jni/Android.mk
index 913a11c6dd16dc1d616611d0626505fe6e0288e5..3e0ce3a3b48ce0cf659f904b7dcad91d12ee2046 100755
--- a/libadblockplus-android/jni/Android.mk
+++ b/libadblockplus-android/jni/Android.mk
@@ -27,7 +27,7 @@ LOCAL_MODULE := libadblockplus-jni
 LOCAL_SRC_FILES := JniLibrary.cpp
 LOCAL_SRC_FILES += JniJsEngine.cpp JniFilterEngine.cpp JniJsValue.cpp
 LOCAL_SRC_FILES += JniFilter.cpp JniSubscription.cpp JniEventCallback.cpp
-LOCAL_SRC_FILES += JniLogSystem.cpp JniWebRequest.cpp
+LOCAL_SRC_FILES += JniLogSystem.cpp JniFileSystem.cpp JniWebRequest.cpp
 LOCAL_SRC_FILES += JniUpdateAvailableCallback.cpp JniUpdateCheckDoneCallback.cpp
 LOCAL_SRC_FILES += JniFilterChangeCallback.cpp JniCallbacks.cpp Utils.cpp
 LOCAL_SRC_FILES += JniNotification.cpp JniShowNotificationCallback.cpp
Index: libadblockplus-android/jni/JniCallbacks.cpp
diff --git a/libadblockplus-android/jni/JniCallbacks.cpp b/libadblockplus-android/jni/JniCallbacks.cpp
index cf46898426ce9af64a8fec5cc392b1c490fd9c24..3191549f44d2141e1cdc87a8fea9fde7f43afa53 100644
--- a/libadblockplus-android/jni/JniCallbacks.cpp
+++ b/libadblockplus-android/jni/JniCallbacks.cpp
@@ -55,12 +55,15 @@ void JniCallbackBase::LogException(JNIEnv* env, jthrowable throwable) const
   }
 }
 
-void JniCallbackBase::CheckAndLogJavaException(JNIEnv* env) const
+bool JniCallbackBase::CheckAndLogJavaException(JNIEnv* env) const
 {
   if (env->ExceptionCheck())
   {
     JniLocalReference<jthrowable> throwable(env, env->ExceptionOccurred());
     env->ExceptionClear();
     LogException(env, *throwable);
+
+    return true;
   }
+  return false;
 }
Index: libadblockplus-android/jni/JniCallbacks.h
diff --git a/libadblockplus-android/jni/JniCallbacks.h b/libadblockplus-android/jni/JniCallbacks.h
index 27269b7cb59419fcc46188a56605d0d60bdc2924..1d12274b14da1f84747d9d917d553cffe5cca343 100644
--- a/libadblockplus-android/jni/JniCallbacks.h
+++ b/libadblockplus-android/jni/JniCallbacks.h
@@ -32,7 +32,7 @@ public:
   JniCallbackBase(JNIEnv* env, jobject callbackObject);
   virtual ~JniCallbackBase();
   void LogException(JNIEnv* env, jthrowable throwable) const;
-  void CheckAndLogJavaException(JNIEnv* env) const;
+  bool CheckAndLogJavaException(JNIEnv* env) const;
 
   JavaVM* GetJavaVM() const
   {
@@ -84,6 +84,18 @@ public:
   void operator()(AdblockPlus::LogSystem::LogLevel logLevel, const std::string& message, const std::string& source);
 };
 
+class JniFileSystemCallback : public JniCallbackBase, public AdblockPlus::FileSystem
+{
+public:
+  JniFileSystemCallback(JNIEnv* env, jobject callbackObject);
+  std::shared_ptr<std::istream> Read(const std::string& path) const;
+  void Write(const std::string& path, std::shared_ptr<std::istream> data);
+  void Move(const std::string& fromPath, const std::string& toPath);
+  void Remove(const std::string& path);
+  AdblockPlus::FileSystem::StatResult Stat(const std::string& path) const;
+  std::string Resolve(const std::string& path) const;
+};
+
 class JniShowNotificationCallback : public JniCallbackBase
 {
 public:
Index: libadblockplus-android/jni/JniFileSystem.cpp
diff --git a/libadblockplus-android/jni/JniFileSystem.cpp b/libadblockplus-android/jni/JniFileSystem.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f87e05752683b8331851dd0a2045061e4e48f574
--- /dev/null
+++ b/libadblockplus-android/jni/JniFileSystem.cpp
@@ -0,0 +1,228 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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/>.
+ */
+
+#include "JniCallbacks.h"
+#include "AdblockPlus/FileSystem.h"
+#include "Utils.h"
+#include <istream>
+#include <streambuf>
+
+class RuntimeErrorWithErrno : public std::runtime_error
+{
+public:
+  explicit RuntimeErrorWithErrno(const std::string& message)
+    : std::runtime_error(message + " (" + strerror(errno) + ")")
+  {
+  }
+};
+
+struct membuf: std::streambuf {
+    membuf(char const* base, size_t size) {
+        char* p(const_cast<char*>(base));
+        this->setg(p, p, p + size);
+    }
+};
+
+struct imemstream: virtual membuf, std::istream {
+    imemstream(char const* base, size_t size)
+        : membuf(base, size)
+        , std::istream(static_cast<std::streambuf*>(this)) {
+    }
+};
+
+// precached in JNI_OnLoad and released in JNI_OnUnload
+JniGlobalReference<jclass>* statResultClass;
+jmethodID existsMethod;
+jmethodID isDirectoryMethod;
+jmethodID isFileMethod;
+jmethodID getLastModifiedMethod;
+
+void JniFileSystem_OnLoad(JavaVM* vm, JNIEnv* env, void* reserved)
+{
+  statResultClass = new JniGlobalReference<jclass>(env, env->FindClass(PKG("FileSystem$StatResult")));
+  existsMethod = env->GetMethodID(statResultClass->Get(), "exists", "()Z");
+  isDirectoryMethod = env->GetMethodID(statResultClass->Get(), "isDirectory", "()Z");
+  isFileMethod = env->GetMethodID(statResultClass->Get(), "isFile", "()Z");
+  getLastModifiedMethod = env->GetMethodID(statResultClass->Get(), "getLastModified", "()J");
+}
+
+void JniFileSystem_OnUnload(JavaVM* vm, JNIEnv* env, void* reserved)
+{
+  if (statResultClass)
+  {
+    delete statResultClass;
+    statResultClass = NULL;
+  }
+}
+
+static jlong JNICALL JniCtor(JNIEnv* env, jclass clazz, jobject callbackObject)
+{
+  try
+  {
+    return JniPtrToLong(new AdblockPlus::FileSystemPtr(new JniFileSystemCallback(env, callbackObject)));
+  }
+  CATCH_THROW_AND_RETURN(env, 0)
+}
+
+static void JNICALL JniDtor(JNIEnv* env, jclass clazz, jlong ptr)
+{
+  delete JniLongToTypePtr<AdblockPlus::FileSystemPtr>(ptr);
+}
+
+JniFileSystemCallback::JniFileSystemCallback(JNIEnv* env, jobject callbackObject)
+  : JniCallbackBase(env, callbackObject)
+{
+}
+
+std::shared_ptr<std::istream> JniFileSystemCallback::Read(const std::string& path) const
+{
+  JNIEnvAcquire env(GetJavaVM());
+
+  jmethodID method = env->GetMethodID(
+    *JniLocalReference<jclass>(*env, env->GetObjectClass(GetCallbackObject())),
+    "read",
+    "(Ljava/lang/String;)[B");
+
+  JniLocalReference<jstring> jPath(*env, env->NewStringUTF(path.c_str()));
+  jbyteArray jData = (jbyteArray)env->CallObjectMethod(GetCallbackObject(), method, *jPath);
+  if (CheckAndLogJavaException(*env))
+    throw new RuntimeErrorWithErrno("Failed to open file (File not found)");
+
+  int dataLength = env->GetArrayLength(jData);
+  char* cData = new char[dataLength];
+  env->GetByteArrayRegion(jData, 0, dataLength, reinterpret_cast<jbyte*>(cData));
+
+  std::shared_ptr<std::istream> cSharedStream(new imemstream(cData, dataLength));
+  return cSharedStream;
+}
+
+void JniFileSystemCallback::Write(const std::string& path, std::shared_ptr<std::istream> dataStreamPtr)
+{
+  JNIEnvAcquire env(GetJavaVM());
+
+  jmethodID method = env->GetMethodID(
+    *JniLocalReference<jclass>(*env, env->GetObjectClass(GetCallbackObject())),
+    "write",
+    "(Ljava/lang/String;[B)V");
+
+  JniLocalReference<jstring> jPath(*env, env->NewStringUTF(path.c_str()));
+
+  // read all the data from the stream into buffer (no appropriate way to pass streams over JNI)
+  std::istream* dataStream = dataStreamPtr.get();
+  dataStream->seekg(0, std::ios::end);
+  int dataLength = dataStream->tellg();
+  char* cData = new char[dataLength];
+  dataStream->seekg(0, std::ios::beg);
+  dataStream->read(cData, dataLength);
+
+  jbyteArray jData = env->NewByteArray(dataLength);
+  env->SetByteArrayRegion(jData, 0, dataLength, reinterpret_cast<jbyte*>(cData));
+
+  env->CallVoidMethod(GetCallbackObject(), method, *jPath, jData);
+  CheckAndLogJavaException(*env);
+  delete[] cData;
+}
+
+void JniFileSystemCallback::Move(const std::string& fromPath, const std::string& toPath)
+{
+  JNIEnvAcquire env(GetJavaVM());
+
+  jmethodID method = env->GetMethodID(
+    *JniLocalReference<jclass>(*env, env->GetObjectClass(GetCallbackObject())),
+    "move",
+    "(Ljava/lang/String;Ljava/lang/String;)V");
+
+  JniLocalReference<jstring> jFromPath(*env, env->NewStringUTF(fromPath.c_str()));
+  JniLocalReference<jstring> jToPath(*env, env->NewStringUTF(toPath.c_str()));
+
+  env->CallVoidMethod(GetCallbackObject(), method, *jFromPath, *jToPath);
+  CheckAndLogJavaException(*env);
+}
+
+void JniFileSystemCallback::Remove(const std::string& path)
+{
+  JNIEnvAcquire env(GetJavaVM());
+
+  jmethodID method = env->GetMethodID(
+    *JniLocalReference<jclass>(*env, env->GetObjectClass(GetCallbackObject())),
+    "remove",
+    "(Ljava/lang/String;)V");
+
+  JniLocalReference<jstring> jPath(*env, env->NewStringUTF(path.c_str()));
+
+  env->CallVoidMethod(GetCallbackObject(), method, *jPath);
+  CheckAndLogJavaException(*env);
+}
+
+AdblockPlus::FileSystem::StatResult JniFileSystemCallback::Stat(const std::string& path) const
+{
+  JNIEnvAcquire env(GetJavaVM());
+
+  jmethodID method = env->GetMethodID(
+    *JniLocalReference<jclass>(*env, env->GetObjectClass(GetCallbackObject())),
+    "stat",
+    "(Ljava/lang/String;)" TYP("FileSystem$StatResult"));
+
+  JniLocalReference<jstring> jPath(*env, env->NewStringUTF(path.c_str()));
+
+  jobject jStatResult = env->CallObjectMethod(GetCallbackObject(), method, *jPath);
+  CheckAndLogJavaException(*env);
+
+  AdblockPlus::FileSystem::StatResult statResult;
+
+  statResult.exists = env->CallBooleanMethod(jStatResult, existsMethod) ? JNI_TRUE : JNI_FALSE;
+  CheckAndLogJavaException(*env);
+
+  statResult.isDirectory = env->CallBooleanMethod(jStatResult, isDirectoryMethod) ? JNI_TRUE : JNI_FALSE;
+  CheckAndLogJavaException(*env);
+
+  statResult.isFile = env->CallBooleanMethod(jStatResult, isFileMethod) ? JNI_TRUE : JNI_FALSE;
+  CheckAndLogJavaException(*env);
+
+  statResult.lastModified = env->CallLongMethod(jStatResult, getLastModifiedMethod);
+  CheckAndLogJavaException(*env);
+
+  return statResult;
+}
+
+std::string JniFileSystemCallback::Resolve(const std::string& path) const
+{
+  JNIEnvAcquire env(GetJavaVM());
+
+  jmethodID method = env->GetMethodID(
+    *JniLocalReference<jclass>(*env, env->GetObjectClass(GetCallbackObject())),
+    "resolve",
+    "(Ljava/lang/String;)Ljava/lang/String;");
+
+  JniLocalReference<jstring> jPath(*env, env->NewStringUTF(path.c_str()));
+
+  jstring jRet = (jstring)env->CallObjectMethod(GetCallbackObject(), method, *jPath);
+  CheckAndLogJavaException(*env);
+
+  return JniJavaToStdString(*env, jRet);
+}
+
+static JNINativeMethod methods[] =
+{
+  { (char*)"ctor", (char*)"(Ljava/lang/Object;)J", (void*)JniCtor },
+  { (char*)"dtor", (char*)"(J)V", (void*)JniDtor }
+};
+
+extern "C" JNIEXPORT void JNICALL Java_org_adblockplus_libadblockplus_FileSystem_registerNatives(JNIEnv *env, jclass clazz)
+{
+  env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]));
+}
Index: libadblockplus-android/jni/JniFileSystem.h
diff --git a/libadblockplus-android/jni/JniWebRequest.h b/libadblockplus-android/jni/JniFileSystem.h
similarity index 78%
copy from libadblockplus-android/jni/JniWebRequest.h
copy to libadblockplus-android/jni/JniFileSystem.h
index 76f36b24c1df9f4dea45afb28d83e17943a3c78f..a5dc3de93fb1c35979f85917ef82f3e8cbe6ce1a 100644
--- a/libadblockplus-android/jni/JniWebRequest.h
+++ b/libadblockplus-android/jni/JniFileSystem.h
@@ -15,13 +15,13 @@
  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef JNIWEBREQUEST_H
-#define JNIWEBREQUEST_H
+#ifndef JNIFILESYSTEM_H
+#define JNIFILESYSTEM_H
 
 #include <jni.h>
 
-void JniWebRequest_OnLoad(JavaVM* vm, JNIEnv* env, void* reserved);
+void JniFileSystem_OnLoad(JavaVM* vm, JNIEnv* env, void* reserved);
 
-void JniWebRequest_OnUnload(JavaVM* vm, JNIEnv* env, void* reserved);
+void JniFileSystem_OnUnload(JavaVM* vm, JNIEnv* env, void* reserved);
 
-#endif /* JNIWEBREQUEST_H */
\ No newline at end of file
+#endif /* JNIFILESYSTEM_H */
\ No newline at end of file
Index: libadblockplus-android/jni/JniJsEngine.cpp
diff --git a/libadblockplus-android/jni/JniJsEngine.cpp b/libadblockplus-android/jni/JniJsEngine.cpp
index d30cb3376830a5ea43fea013a4e6c4b08b58bea4..ad170a7ce58714005c52922b79b187a949cd92ab 100644
--- a/libadblockplus-android/jni/JniJsEngine.cpp
+++ b/libadblockplus-android/jni/JniJsEngine.cpp
@@ -136,6 +136,18 @@ static void JNICALL JniSetDefaultFileSystem(JNIEnv* env, jclass clazz, jlong ptr
   CATCH_AND_THROW(env)
 }
 
+static void JNICALL JniSetFileSystem(JNIEnv* env, jclass clazz, jlong ptr, jlong fileSystemPtr)
+{
+  AdblockPlus::JsEnginePtr& engine = *JniLongToTypePtr<AdblockPlus::JsEnginePtr>(ptr);
+
+  try
+  {
+    AdblockPlus::FileSystemPtr fileSystem = *JniLongToTypePtr<AdblockPlus::FileSystemPtr>(fileSystemPtr);
+    engine->SetFileSystem(fileSystem);
+  }
+  CATCH_AND_THROW(env)
+}
+
 static void JNICALL JniSetDefaultWebRequest(JNIEnv* env, jclass clazz, jlong ptr)
 {
   AdblockPlus::JsEnginePtr& engine = *JniLongToTypePtr<AdblockPlus::JsEnginePtr>(ptr);
@@ -242,6 +254,7 @@ static JNINativeMethod methods[] =
 
   { (char*)"evaluate", (char*)"(JLjava/lang/String;Ljava/lang/String;)" TYP("JsValue"), (void*)JniEvaluate },
 
+  { (char*)"setFileSystem", (char*)"(JJ)V", (void*)JniSetFileSystem },
   { (char*)"setDefaultFileSystem", (char*)"(JLjava/lang/String;)V", (void*)JniSetDefaultFileSystem },
   { (char*)"setLogSystem", (char*)"(JJ)V", (void*)JniSetLogSystem },
   { (char*)"setDefaultLogSystem", (char*)"(J)V", (void*)JniSetDefaultLogSystem },
Index: libadblockplus-android/jni/JniLibrary.cpp
diff --git a/libadblockplus-android/jni/JniLibrary.cpp b/libadblockplus-android/jni/JniLibrary.cpp
index 27da9be2588237c1b0abd3524882c6c7a2f83978..2e5812b6efba9ad3e5df46ea94ad68813a3d5491 100644
--- a/libadblockplus-android/jni/JniLibrary.cpp
+++ b/libadblockplus-android/jni/JniLibrary.cpp
@@ -21,6 +21,7 @@
 #include "JniCallbacks.h"
 #include "JniNotification.h"
 #include "JniWebRequest.h"
+#include "JniFileSystem.h"
 #include "Utils.h"
 
 jint JNI_OnLoad(JavaVM* vm, void* reserved)
@@ -37,6 +38,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
   JniCallbacks_OnLoad(vm, env, reserved);
   JniNotification_OnLoad(vm, env, reserved);
   JniWebRequest_OnLoad(vm, env, reserved);
+  JniFileSystem_OnLoad(vm, env, reserved);
   JniUtils_OnLoad(vm, env, reserved);
 
   return ABP_JNI_VERSION;
@@ -56,5 +58,6 @@ void JNI_OnUnload(JavaVM* vm, void* reserved)
   JniCallbacks_OnUnload(vm, env, reserved);
   JniNotification_OnUnload(vm, env, reserved);
   JniWebRequest_OnUnload(vm, env, reserved);
+  JniFileSystem_OnUnload(vm, env, reserved);
   JniUtils_OnUnload(vm, env, reserved);
 }
\ No newline at end of file
Index: libadblockplus-android/jni/Utils.h
diff --git a/libadblockplus-android/jni/Utils.h b/libadblockplus-android/jni/Utils.h
index a79518cacc76b3541dbcd649a77fdb1bc9ccdc01..46a6bdef1d30f608fb45a106fbb2de1f84ea4eb7 100644
--- a/libadblockplus-android/jni/Utils.h
+++ b/libadblockplus-android/jni/Utils.h
@@ -31,6 +31,14 @@
 
 #define ABP_JNI_VERSION JNI_VERSION_1_6
 
+namespace AdblockPlus
+{
+  namespace Utils
+  {
+    std::string Slurp(std::istream& stream);
+  }
+}
+
 void JniUtils_OnLoad(JavaVM* vm, JNIEnv* env, void* reserved);
 
 void JniUtils_OnUnload(JavaVM* vm, JNIEnv* env, void* reserved);
Index: libadblockplus-android/src/org/adblockplus/libadblockplus/FileSystem.java
diff --git a/libadblockplus-android/src/org/adblockplus/libadblockplus/FileSystem.java b/libadblockplus-android/src/org/adblockplus/libadblockplus/FileSystem.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd24dc481e42604a70a02926ab7129d0417e93f7
--- /dev/null
+++ b/libadblockplus-android/src/org/adblockplus/libadblockplus/FileSystem.java
@@ -0,0 +1,155 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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.libadblockplus;
+
+public abstract class FileSystem implements Disposable
+{
+  private final Disposer disposer;
+  protected final long ptr;
+
+  static
+  {
+    System.loadLibrary("adblockplus-jni");
+    registerNatives();
+  }
+
+  public FileSystem()
+  {
+    this.ptr = ctor(this);
+    this.disposer = new Disposer(this, new DisposeWrapper(this.ptr));
+  }
+
+  public static final class StatResult
+  {
+    private boolean exists;
+
+    public boolean exists()
+    {
+      return exists;
+    }
+
+    private boolean isDirectory;
+
+    public boolean isDirectory()
+    {
+      return isDirectory;
+    }
+
+    private boolean isFile;
+
+    public boolean isFile()
+    {
+      return isFile;
+    }
+
+    private long lastModified;
+
+    public long getLastModified()
+    {
+      return lastModified;
+    }
+
+    public StatResult(boolean exists, boolean isDirectory, boolean isFile, long lastModified)
+    {
+      this.exists = exists;
+      this.isDirectory = isDirectory;
+      this.isFile = isFile;
+      this.lastModified = lastModified;
+    }
+
+    @Override
+    public String toString()
+    {
+      return "StatResult{" +
+        "exists=" + exists +
+        ", isDirectory=" + isDirectory +
+        ", isFile=" + isFile +
+        ", lastModified=" + lastModified +
+        '}';
+    }
+  }
+
+  /**
+   * Reads from a file.
+   * @param path File path.
+   * @return File's binary data.
+   */
+  public abstract byte[] read(String path);
+
+  /**
+   * Writes to a file.
+   * @param path File path.
+   * @param data File's binary data to write.
+   */
+  public abstract void write(String path, byte[] data);
+
+  /**
+   * Moves a file (i.e.\ renames it).
+   * @param fromPath Current path to the file.
+   * @param toPath New path to the file.
+   */
+  public abstract void move(String fromPath, String toPath);
+
+  /**
+   * Removes a file.
+   * @param path File path.
+   */
+  public abstract void remove(String path);
+
+  /**
+   * Retrieves information about a file.
+   * @param path File path.
+   * @return File information.
+   */
+  public abstract StatResult stat(String path);
+
+  /**
+   * Returns the absolute path to a file.
+   * @param path File path (can be relative or absolute).
+   * @return Absolute file path.
+   */
+  public abstract String resolve(String path);
+
+  @Override
+  public void dispose()
+  {
+    this.disposer.dispose();
+  }
+
+  private final static class DisposeWrapper implements Disposable
+  {
+    private final long ptr;
+
+    public DisposeWrapper(final long ptr)
+    {
+      this.ptr = ptr;
+    }
+
+    @Override
+    public void dispose()
+    {
+      dtor(this.ptr);
+    }
+  }
+
+  private final static native void registerNatives();
+
+  private final static native long ctor(Object callbackObject);
+
+  private final static native void dtor(long ptr);
+}
Index: libadblockplus-android/src/org/adblockplus/libadblockplus/FileSystemUtils.java
diff --git a/libadblockplus-android/src/org/adblockplus/libadblockplus/FileSystemUtils.java b/libadblockplus-android/src/org/adblockplus/libadblockplus/FileSystemUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..d529209a08a5b15c39ad59c2e1ed756b3beba1ff
--- /dev/null
+++ b/libadblockplus-android/src/org/adblockplus/libadblockplus/FileSystemUtils.java
@@ -0,0 +1,135 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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.libadblockplus;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+public class FileSystemUtils
+{
+
+  private static byte[] readStream(InputStream inputStream, int length) throws IOException
+  {
+    byte[] data = new byte[length];
+    int bytesRead, totalBytesRead = 0;
+    while ((bytesRead = inputStream.read(data, totalBytesRead, length - totalBytesRead)) > 0)
+    {
+      totalBytesRead += bytesRead;
+    }
+    return data;
+  }
+
+  /**
+   * Convert java signed int to byte
+   * @param b int representation of byte
+   * @return byte representation of byte
+   */
+  public static byte byteFromInt(int b)
+  {
+    return (byte)(b & 0xFF);
+  }
+
+  /**
+   * Read all the file data to string
+   *
+   * @param file path to read data
+   * @return file data
+   * @throws java.io.IOException
+   */
+  public static byte[] readFile(File file) throws IOException
+  {
+    FileInputStream fileInputStream = new FileInputStream(file);
+    try
+    {
+      return readStream(fileInputStream, (int) file.length());
+    }
+    finally
+    {
+      try
+      {
+        fileInputStream.close();
+      }
+      catch (IOException e)
+      {
+        // ignored
+      }
+    }
+  }
+
+  /**
+   * Write data to file (with rewriting)
+   *
+   * @param file file
+   * @param data file data
+   */
+  public static void writeFile(File file, byte[] data) throws IOException
+  {
+    FileOutputStream fos = null;
+    try
+    {
+      fos = new FileOutputStream(file);
+      if (data != null)
+      {
+        fos.write(data);
+      }
+    }
+    finally
+    {
+      if (fos != null)
+      {
+        try
+        {
+          fos.close();
+        }
+        catch (IOException e)
+        {
+          // ignored
+        }
+      }
+    }
+  }
+
+  /**
+   * Generate unique filename
+   *
+   * @param prefix prefix
+   * @param suffix suffix
+   * @return generated unique filename
+   */
+  public static String generateUniqueFileName(String prefix, String suffix)
+  {
+    StringBuilder sb = new StringBuilder();
+    if (prefix != null)
+    {
+      sb.append(prefix);
+    }
+
+    sb.append(UUID.randomUUID().toString());
+
+    if (suffix != null)
+    {
+      sb.append(suffix);
+    }
+
+    return sb.toString();
+  }
+}
Index: libadblockplus-android/src/org/adblockplus/libadblockplus/JsEngine.java
diff --git a/libadblockplus-android/src/org/adblockplus/libadblockplus/JsEngine.java b/libadblockplus-android/src/org/adblockplus/libadblockplus/JsEngine.java
index 3b806122759d8dd8b2d496868bbf401703cf0a1b..a0eed87459be205faaf55dd562125eb0a81529db 100644
--- a/libadblockplus-android/src/org/adblockplus/libadblockplus/JsEngine.java
+++ b/libadblockplus-android/src/org/adblockplus/libadblockplus/JsEngine.java
@@ -78,6 +78,11 @@ public final class JsEngine implements Disposable
     triggerEvent(this.ptr, eventName, null);
   }
 
+  public void setFileSystem(FileSystem fileSystem)
+  {
+    setFileSystem(this.ptr, fileSystem.ptr);
+  }
+
   public void setDefaultFileSystem(final String basePath)
   {
     setDefaultFileSystem(this.ptr, basePath);
@@ -152,6 +157,8 @@ public final class JsEngine implements Disposable
 
   private final static native void triggerEvent(long ptr, String eventName, long[] args);
 
+  private final static native void setFileSystem(long ptr, long fileSystemPtr);
+
   private final static native void setDefaultFileSystem(long ptr, String basePath);
 
   private final static native void setLogSystem(long ptr, long logSystemPtr);
Index: libadblockplus-android/src/org/adblockplus/libadblockplus/android/AndroidFileSystem.java
diff --git a/libadblockplus-android/src/org/adblockplus/libadblockplus/android/AndroidFileSystem.java b/libadblockplus-android/src/org/adblockplus/libadblockplus/android/AndroidFileSystem.java
new file mode 100644
index 0000000000000000000000000000000000000000..d1f3bf709a57420890ab7cb909c11ace960a00ab
--- /dev/null
+++ b/libadblockplus-android/src/org/adblockplus/libadblockplus/android/AndroidFileSystem.java
@@ -0,0 +1,140 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2017 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.libadblockplus.android;
+
+import org.adblockplus.libadblockplus.AdblockPlusException;
+import org.adblockplus.libadblockplus.FileSystem;
+import org.adblockplus.libadblockplus.FileSystemUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * AndroidFileSystem is inefficient for production files routines
+ * as C++ streams can't pass JNI layer and be converted into Java streams and vice versa.
+ *
+ * So in case of any stream routines full stream content is read in either C++ or Java side
+ * and it's being passed as bytes array though JNI layer
+ *
+ * AndroidFileSystem is meant to be used in tests mostly
+ *
+ * All paths are considered relative to the base path, or to be absolute
+ * (see `resolve(String path)`) if no base path is set (see `AndroidFileSystem(File basePath)`)
+ */
+public class AndroidFileSystem extends FileSystem
+{
+  private File basePath;
+
+  public File getBasePath()
+  {
+    return basePath;
+  }
+
+  public AndroidFileSystem()
+  {
+  }
+
+  /*
+   * Sets the base path, all paths are considered relative to it.
+   * @param basePath base path
+   */
+  public AndroidFileSystem(File basePath)
+  {
+    this();
+    this.basePath = basePath;
+  }
+
+  @Override
+  public byte[] read(String path)
+  {
+    File file = new File(path);
+    if (!file.exists())
+    {
+      return null;
+    }
+
+    try
+    {
+      return FileSystemUtils.readFile(file);
+    }
+    catch (IOException e)
+    {
+      throw new AdblockPlusException(e);
+    }
+  }
+
+  @Override
+  public void write(String path, byte[] data)
+  {
+    File file = new File(path);
+    if (file.exists())
+    {
+      file.delete();
+    }
+
+    try
+    {
+      FileSystemUtils.writeFile(file, data);
+    }
+    catch (IOException e)
+    {
+      throw new AdblockPlusException(e);
+    }
+  }
+
+  @Override
+  public void move(String fromPath, String toPath)
+  {
+    File fromFile = new File(fromPath);
+    if (!fromFile.exists())
+      throw new AdblockPlusException("File does not exist: " + fromPath);
+
+    File toFile = new File(toPath);
+    if (!fromFile.renameTo(toFile))
+    {
+      throw new AdblockPlusException("Failed to move " + fromPath + " to " + toFile);
+    }
+  }
+
+  @Override
+  public void remove(String path)
+  {
+    File file = new File(path);
+    if (file.exists())
+    {
+      file.delete();
+    }
+  }
+
+  @Override
+  public StatResult stat(String path)
+  {
+    File file = new File(path);
+    return new StatResult(
+      file.exists(),
+      file.isDirectory(),
+      file.isFile(),
+      file.lastModified());
+  }
+
+  @Override
+  public String resolve(String path)
+  {
+    return (basePath != null ? new File(basePath, path).getAbsolutePath() : path);
+  }
+}
