Mholloway has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/317281

Change subject: Retrofit editing
......................................................................

Retrofit editing

Change-Id: I223090e67bb6206817041ce3669281dbae3cfdff
---
D app/src/androidTest/java/org/wikipedia/editing/EditPreviewTaskTest.java
A app/src/androidTest/java/org/wikipedia/editing/EditPreviewTest.java
A app/src/androidTest/java/org/wikipedia/editing/EditTest.java
A app/src/androidTest/java/org/wikipedia/editing/FetchSectionWikitextTests.java
M app/src/androidTest/java/org/wikipedia/test/CreateAccountTaskTest.java
D app/src/androidTest/java/org/wikipedia/test/EditTaskTest.java
D app/src/androidTest/java/org/wikipedia/test/FetchSectionWikitextTaskTests.java
A app/src/main/java/org/wikipedia/captcha/Captcha.java
A app/src/main/java/org/wikipedia/captcha/CaptchaClient.java
R app/src/main/java/org/wikipedia/captcha/CaptchaHandler.java
R app/src/main/java/org/wikipedia/captcha/CaptchaResult.java
A app/src/main/java/org/wikipedia/editing/Edit.java
A app/src/main/java/org/wikipedia/editing/EditClient.java
A app/src/main/java/org/wikipedia/editing/EditPreview.java
M app/src/main/java/org/wikipedia/editing/EditPreviewFragment.java
D app/src/main/java/org/wikipedia/editing/EditPreviewTask.java
M app/src/main/java/org/wikipedia/editing/EditSectionActivity.java
D app/src/main/java/org/wikipedia/editing/EditTask.java
A app/src/main/java/org/wikipedia/editing/EditToken.java
M app/src/main/java/org/wikipedia/editing/EditTokenStorage.java
D app/src/main/java/org/wikipedia/editing/FetchEditTokenTask.java
D app/src/main/java/org/wikipedia/editing/FetchSectionWikitextTask.java
D app/src/main/java/org/wikipedia/editing/RefreshCaptchaTask.java
A app/src/main/java/org/wikipedia/editing/Wikitext.java
A app/src/main/java/org/wikipedia/server/mwapi/ApiResponsePage.java
25 files changed, 890 insertions(+), 653 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/android/wikipedia 
refs/changes/81/317281/1

diff --git 
a/app/src/androidTest/java/org/wikipedia/editing/EditPreviewTaskTest.java 
b/app/src/androidTest/java/org/wikipedia/editing/EditPreviewTaskTest.java
deleted file mode 100644
index cc24ec9..0000000
--- a/app/src/androidTest/java/org/wikipedia/editing/EditPreviewTaskTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.wikipedia.editing;
-
-import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.wikipedia.dataclient.WikiSite;
-import org.wikipedia.WikipediaApp;
-import org.wikipedia.page.PageTitle;
-import org.wikipedia.testlib.TestLatch;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class EditPreviewTaskTest {
-    @Test
-    public void testPreview() throws Throwable {
-        final PageTitle title = new PageTitle(null, 
"Test_page_for_app_testing/Section1", WikiSite.forLanguageCode("test"));
-        long randomTime = System.currentTimeMillis();
-        final String wikiText = "== Section 2 ==\n\nEditing section INSERT 
RANDOM & HERE test at " + randomTime;
-
-        String result = Subject.execute(wikiText, title);
-        assertThat(result, containsString(String.valueOf(randomTime)));
-    }
-
-    private static class Subject extends EditPreviewTask {
-        public static String execute(String wikiText, PageTitle title) {
-            Subject subject = new Subject(wikiText, title);
-            subject.execute();
-            return subject.await();
-        }
-
-        @NonNull private final TestLatch latch = new TestLatch();
-        private String result;
-
-        Subject(String wikiText, PageTitle title) {
-            super(WikipediaApp.getInstance(), wikiText, title);
-        }
-
-        @Override
-        public void onFinish(String result) {
-            super.onFinish(result);
-            this.result = result;
-            latch.countDown();
-        }
-
-        public String await() {
-            latch.await();
-            return result;
-        }
-    }
-}
diff --git 
a/app/src/androidTest/java/org/wikipedia/editing/EditPreviewTest.java 
b/app/src/androidTest/java/org/wikipedia/editing/EditPreviewTest.java
new file mode 100644
index 0000000..8ac3187
--- /dev/null
+++ b/app/src/androidTest/java/org/wikipedia/editing/EditPreviewTest.java
@@ -0,0 +1,36 @@
+package org.wikipedia.editing;
+
+import android.support.annotation.NonNull;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.page.PageTitle;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+
+@RunWith(AndroidJUnit4.class)
+public class EditPreviewTest {
+    @Test
+    public void testPreview() throws Throwable {
+        final WikiSite wiki = new WikiSite("test.wikipedia.org");
+        final PageTitle title = new PageTitle(null, 
"Test_page_for_app_testing/Section1", wiki);
+        final long randomTime = System.currentTimeMillis();
+        final String wikiText = "== Section 2 ==\n\nEditing section INSERT 
RANDOM & HERE test at " + randomTime;
+
+        new EditClient().previewEdit(wiki, title, wikiText,
+                new EditPreview.Callback() {
+                    @Override
+                    public void success(@NonNull String preview) {
+                        assertThat(preview, 
containsString(String.valueOf(randomTime)));
+                    }
+
+                    @Override
+                    public void failure(@NonNull Throwable caught) {
+                        throw new RuntimeException(caught);
+                    }
+                });
+    }
+}
diff --git a/app/src/androidTest/java/org/wikipedia/editing/EditTest.java 
b/app/src/androidTest/java/org/wikipedia/editing/EditTest.java
new file mode 100644
index 0000000..d11e966
--- /dev/null
+++ b/app/src/androidTest/java/org/wikipedia/editing/EditTest.java
@@ -0,0 +1,191 @@
+package org.wikipedia.editing;
+
+import android.support.annotation.NonNull;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.wikipedia.WikipediaApp;
+import org.wikipedia.captcha.CaptchaResult;
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.page.PageTitle;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+@RunWith(AndroidJUnit4.class)
+public class EditTest {
+    private static final WikiSite TEST_WIKI_SITE = new 
WikiSite("test.wikipedia.org");
+    private static final String ABUSE_FILTER_ERROR_PAGE_TITLE = 
"Test_page_for_app_testing/AbuseFilter";
+    private static final int DEFAULT_SECTION_ID = 0;
+    // https://www.mediawiki.org/wiki/Manual:Edit_token#The_edit_token_suffix
+    private static final String ANONYMOUS_TOKEN = "+\\";
+    private static final String DEFAULT_SUMMARY = "";
+
+    @Before
+    public void setUp() {
+        // Cookies for a logged in session cannot be used with the anonymous 
edit token.
+        app().getCookieManager().clearAllCookies();
+    }
+
+    @Test
+    public void testEdit() {
+        PageTitle title = new PageTitle(null, 
"Test_page_for_app_testing/Section1", TEST_WIKI_SITE);
+        String wikitext = "== Section 2 ==\n\nEditing section INSERT RANDOM & 
HERE test at "
+                + System.currentTimeMillis();
+        final int sectionId = 3;
+
+        service.edit(TEST_WIKI_SITE, title, sectionId, wikitext, 
ANONYMOUS_TOKEN, DEFAULT_SUMMARY,
+                false, null, null, new Edit.Callback() {
+                    @Override
+                    public void success(@NonNull EditingResult result) {
+                        assertThat(result, 
instanceOf(SuccessEditResult.class));
+                    }
+
+                    @Override
+                    public void failure(@NonNull Throwable caught) {
+                        throw new RuntimeException(caught);
+                    }
+                });
+    }
+
+    @Test
+    public void testCaptcha() {
+        PageTitle title = new PageTitle(null, 
"Test_page_for_app_testing/Captcha", TEST_WIKI_SITE);
+        String wikitext = "== Section 2 ==\n\nEditing by inserting an external 
link https://";
+                + System.currentTimeMillis();
+
+        service.edit(TEST_WIKI_SITE, title, DEFAULT_SECTION_ID, wikitext, 
ANONYMOUS_TOKEN,
+                DEFAULT_SUMMARY, false, null, null, new Edit.Callback() {
+                    @Override
+                    public void success(@NonNull EditingResult result) {
+                        validateCaptcha(result);
+                    }
+
+                    @Override
+                    public void failure(@NonNull Throwable caught) {
+                        throw new RuntimeException(caught);
+                    }
+                });
+    }
+
+    /**
+     * Test handling of abuse filter warnings which warn users about making 
edits of a certain kind.
+     *
+     * Type:   warn
+     * Action: editing any userspace page while logged out
+     * Filter: https://test.wikipedia.org/wiki/Special:AbuseFilter/94
+     */
+    @Test
+    public void testAbuseFilterTriggerWarn() {
+        PageTitle title = new PageTitle(null, "User:Yuvipandaaaaaaaa", 
TEST_WIKI_SITE);
+
+        // Rule 94 is only a warning so the initial attempt may be successful. 
The second is
+        // guaranteed to be a warning if different content is used. 
@FlakyTest(tolerance = 2)
+        // doesn't work with JUnit 4.
+        for (int i = 0; i < 2; ++i) {
+            String wikitext = "Testing Abusefilter by simply editing this 
page. Triggering rule 94"
+                    + "at " + System.nanoTime();
+
+            service.edit(TEST_WIKI_SITE, title, DEFAULT_SECTION_ID, wikitext, 
ANONYMOUS_TOKEN,
+                    DEFAULT_SUMMARY, false, null, null, new Edit.Callback() {
+                        @Override
+                        public void success(@NonNull EditingResult result) {
+                            if (!(result instanceof SuccessEditResult)) {
+                                assertThat(result, 
instanceOf(AbuseFilterEditResult.class));
+                                //noinspection ConstantConditions
+                                assertThat(((AbuseFilterEditResult) 
result).getType(),
+                                        
is(AbuseFilterEditResult.TYPE_WARNING));
+                            }
+                        }
+
+                        @Override
+                        public void failure(@NonNull Throwable caught) {
+                            throw new RuntimeException(caught);
+                        }
+                    });
+        }
+    }
+
+    /**
+     * Test handling of abuse filter errors which completely disallow edits of 
a certain kind.
+     *
+     * Type:   disallow
+     * Action: adding string "poop" to page text
+     * Filter: https://test.wikipedia.org/wiki/Special:AbuseFilter/2
+     */
+    @Test
+    public void testAbuseFilterTriggerStop() {
+        PageTitle title = new PageTitle(null, ABUSE_FILTER_ERROR_PAGE_TITLE, 
TEST_WIKI_SITE);
+        String wikitext = "== Section 2 ==\n\nTriggering AbuseFilter number 2 
by saying poop many"
+                + "times at " + System.currentTimeMillis();
+
+        service.edit(TEST_WIKI_SITE, title, DEFAULT_SECTION_ID, wikitext, 
ANONYMOUS_TOKEN,
+                DEFAULT_SUMMARY, false, null, null, new Edit.Callback() {
+                    @Override
+                    public void success(@NonNull EditingResult result) {
+                        assertThat(result, 
instanceOf(AbuseFilterEditResult.class));
+                        assertThat(((AbuseFilterEditResult) result).getType(),
+                                is(AbuseFilterEditResult.TYPE_ERROR));
+                    }
+
+                    @Override
+                    public void failure(@NonNull Throwable caught) {
+                        throw new RuntimeException(caught);
+                    }
+                });
+    }
+
+    /**
+     * Test the app's handling of the abuse filter emitting arbitrary error 
codes.
+     *
+     * Type:   warn
+     * Action: adding string "appcrashtest" to page text
+     * Filter: https://test.wikipedia.org/wiki/Special:AbuseFilter/152
+     */
+    @Test
+    public void testAbuseFilterTriggerStopOnArbitraryErrorCode() {
+        PageTitle title = new PageTitle(null, ABUSE_FILTER_ERROR_PAGE_TITLE, 
TEST_WIKI_SITE);
+        String wikitext = "== Section 2 ==\n\nTriggering AbuseFilter number 
152 by saying"
+                + "appcrashtest many times at " + System.currentTimeMillis();
+
+        service.edit(TEST_WIKI_SITE, title, DEFAULT_SECTION_ID, wikitext, 
ANONYMOUS_TOKEN,
+                DEFAULT_SUMMARY, false, null, null, new Edit.Callback() {
+                    @Override
+                    public void success(@NonNull EditingResult result) {
+                        assertThat(result, 
instanceOf(AbuseFilterEditResult.class));
+                        assertThat(((AbuseFilterEditResult) result).getType(),
+                                is(AbuseFilterEditResult.TYPE_WARNING));
+                    }
+
+                    @Override
+                    public void failure(@NonNull Throwable caught) {
+                        throw new RuntimeException(caught);
+                    }
+                });
+    }
+
+    private void validateCaptcha(EditingResult result) {
+        assertThat(result, instanceOf(CaptchaResult.class));
+        CaptchaResult captchaResult = (CaptchaResult) result;
+        String url = captchaResult.getCaptchaUrl(TEST_WIKI_SITE);
+        assertThat(isValidCaptchaUrl(url), is(true));
+    }
+
+    private boolean isValidCaptchaUrl(String url) {
+        return url.startsWith(getNetworkProtocol()
+                + 
"://test.wikipedia.org/w/index.php?title=Special:Captcha/image");
+    }
+
+    private String getNetworkProtocol() {
+        return app().getWikiSite().scheme();
+    }
+
+    private WikipediaApp app() {
+        return WikipediaApp.getInstance();
+    }
+
+    private EditClient service = new EditClient();
+}
diff --git 
a/app/src/androidTest/java/org/wikipedia/editing/FetchSectionWikitextTests.java 
b/app/src/androidTest/java/org/wikipedia/editing/FetchSectionWikitextTests.java
new file mode 100644
index 0000000..30d1e9d
--- /dev/null
+++ 
b/app/src/androidTest/java/org/wikipedia/editing/FetchSectionWikitextTests.java
@@ -0,0 +1,31 @@
+package org.wikipedia.editing;
+
+import android.support.annotation.NonNull;
+import android.test.ActivityUnitTestCase;
+
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.page.PageTitle;
+import org.wikipedia.test.TestDummyActivity;
+
+public class FetchSectionWikitextTests extends 
ActivityUnitTestCase<TestDummyActivity> {
+    public FetchSectionWikitextTests() {
+        super(TestDummyActivity.class);
+    }
+
+    public void testPageFetch() throws Throwable {
+        WikiSite wiki = new WikiSite("test.wikipedia.org");
+        PageTitle title = new PageTitle(null, 
"Test_page_for_app_testing/Section1", wiki);
+        new EditClient().wikitext(wiki, title, 2,
+                new Wikitext.Callback() {
+                    @Override
+                    public void success(@NonNull String wikitext) {
+                        assertNotNull(wikitext);
+                    }
+
+                    @Override
+                    public void failure(@NonNull Throwable caught) {
+                        throw new RuntimeException(caught);
+                    }
+                });
+    }
+}
diff --git 
a/app/src/androidTest/java/org/wikipedia/test/CreateAccountTaskTest.java 
b/app/src/androidTest/java/org/wikipedia/test/CreateAccountTaskTest.java
index 82851a2..90e53d1 100644
--- a/app/src/androidTest/java/org/wikipedia/test/CreateAccountTaskTest.java
+++ b/app/src/androidTest/java/org/wikipedia/test/CreateAccountTaskTest.java
@@ -16,7 +16,7 @@
 import org.wikipedia.dataclient.WikiSite;
 import org.wikipedia.createaccount.CreateAccountInfoResult;
 import org.wikipedia.createaccount.CreateAccountInfoTask;
-import org.wikipedia.editing.CaptchaResult;
+import org.wikipedia.captcha.CaptchaResult;
 import org.wikipedia.testlib.TestLatch;
 import org.wikipedia.util.log.L;
 
diff --git a/app/src/androidTest/java/org/wikipedia/test/EditTaskTest.java 
b/app/src/androidTest/java/org/wikipedia/test/EditTaskTest.java
deleted file mode 100644
index edb5c45..0000000
--- a/app/src/androidTest/java/org/wikipedia/test/EditTaskTest.java
+++ /dev/null
@@ -1,187 +0,0 @@
-package org.wikipedia.test;
-
-import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.wikipedia.WikipediaApp;
-import org.wikipedia.dataclient.WikiSite;
-import org.wikipedia.editing.AbuseFilterEditResult;
-import org.wikipedia.editing.CaptchaResult;
-import org.wikipedia.editing.EditTask;
-import org.wikipedia.editing.EditingResult;
-import org.wikipedia.editing.SuccessEditResult;
-import org.wikipedia.page.PageTitle;
-import org.wikipedia.testlib.TestLatch;
-
-import static android.support.test.InstrumentationRegistry.getTargetContext;
-import static org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class EditTaskTest {
-    private static final WikiSite TEST_WIKI = WikiSite.forLanguageCode("test");
-
-    private static final String ABUSE_FILTER_ERROR_PAGE_TITLE = 
"Test_page_for_app_testing/AbuseFilter";
-
-    @Before
-    public void setUp() {
-        // Cookies for a logged in session cannot be used with the anonymous 
edit token.
-        app().getCookieManager().clearAllCookies();
-    }
-
-    @Test
-    public void testEdit() {
-        PageTitle title = new PageTitle(null, 
"Test_page_for_app_testing/Section1", TEST_WIKI);
-        String wikitext = "== Section 2 ==\n\nEditing section INSERT RANDOM & 
HERE test at "
-                + System.currentTimeMillis();
-        final int sectionId = 3;
-
-        EditingResult result = Subject.execute(title, wikitext, sectionId);
-        assertThat(result, instanceOf(SuccessEditResult.class));
-    }
-
-    @Test
-    public void testCaptcha() {
-        PageTitle title = new PageTitle(null, 
"Test_page_for_app_testing/Captcha", TEST_WIKI);
-        String wikitext = "== Section 2 ==\n\nEditing by inserting an external 
link https://";
-                + System.currentTimeMillis();
-
-        EditingResult result = Subject.execute(title, wikitext);
-        validateCaptcha(result);
-    }
-
-    /**
-     * Test handling of abuse filter warnings which warn users about making 
edits of a certain kind.
-     *
-     * Type:   warn
-     * Action: editing any userspace page while logged out
-     * Filter: https://test.wikipedia.org/wiki/Special:AbuseFilter/94
-     */
-    @Test
-    public void testAbuseFilterTriggerWarn() {
-        PageTitle title = new PageTitle(null, "User:Yuvipandaaaaaaaa", 
TEST_WIKI);
-
-        // Rule 94 is only a warning so the initial attempt may be successful. 
The second is
-        // guaranteed to be a warning if different content is used. 
@FlakyTest(tolerance = 2)
-        // doesn't work with JUnit 4.
-        EditingResult result = null;
-        for (int i = 0; !(result instanceof AbuseFilterEditResult) && i < 2; 
++i) {
-            String wikitext = "Testing Abusefilter by simply editing this 
page. Triggering rule 94 at "
-                    + System.nanoTime();
-            result = Subject.execute(title, wikitext);
-        }
-
-        assertThat(result, instanceOf(AbuseFilterEditResult.class));
-        // todo: test against a warning, not an error. Rule 94 is currently 
returning
-        //       abusefilter-disallowed instead of abusefilter-warning. 
EditTask needs to be
-        //       reworked to a data client that can consume mock data
-        //noinspection ConstantConditions
-        assertThat(((AbuseFilterEditResult) result).getType(), 
is(AbuseFilterEditResult.TYPE_ERROR));
-    }
-
-    /**
-     * Test handling of abuse filter errors which completely disallow edits of 
a certain kind.
-     *
-     * Type:   disallow
-     * Action: adding string "poop" to page text
-     * Filter: https://test.wikipedia.org/wiki/Special:AbuseFilter/2
-     */
-    @Test
-    public void testAbuseFilterTriggerStop() {
-        PageTitle title = new PageTitle(null, ABUSE_FILTER_ERROR_PAGE_TITLE, 
TEST_WIKI);
-        String wikitext = "== Section 2 ==\n\nTriggering AbuseFilter number 2 
by saying poop many times at "
-                + System.currentTimeMillis();
-
-        EditingResult result = Subject.execute(title, wikitext);
-        assertThat(result, instanceOf(AbuseFilterEditResult.class));
-        assertThat(((AbuseFilterEditResult) result).getType(), 
is(AbuseFilterEditResult.TYPE_ERROR));
-    }
-
-    /**
-     * Test the app's handling of the abuse filter emitting arbitrary error 
codes.
-     *
-     * Type:   warn
-     * Action: adding string "appcrashtest" to page text
-     * Filter: https://test.wikipedia.org/wiki/Special:AbuseFilter/152
-     */
-    @Test
-    public void testAbuseFilterTriggerStopOnArbitraryErrorCode() {
-        PageTitle title = new PageTitle(null, ABUSE_FILTER_ERROR_PAGE_TITLE, 
TEST_WIKI);
-        String wikitext = "== Section 2 ==\n\nTriggering AbuseFilter number 
152 by saying appcrashtest many times at "
-                + System.currentTimeMillis();
-
-        EditingResult result = Subject.execute(title, wikitext);
-        assertThat(result, instanceOf(AbuseFilterEditResult.class));
-        assertThat(((AbuseFilterEditResult) result).getType(), 
is(AbuseFilterEditResult.TYPE_ERROR));
-    }
-
-    private void validateCaptcha(EditingResult result) {
-        assertThat(result, instanceOf(CaptchaResult.class));
-        CaptchaResult captchaResult = (CaptchaResult) result;
-        String url = captchaResult.getCaptchaUrl(TEST_WIKI);
-        assertThat(isValidCaptchaUrl(url), is(true));
-    }
-
-    private boolean isValidCaptchaUrl(String url) {
-        return url.startsWith(getNetworkProtocol()
-                + 
"://test.wikipedia.org/w/index.php?title=Special:Captcha/image");
-    }
-
-    private String getNetworkProtocol() {
-        return app().getWikiSite().scheme();
-    }
-
-    private WikipediaApp app() {
-        return WikipediaApp.getInstance();
-    }
-
-    private static class Subject extends EditTask {
-        private static final int DEFAULT_SECTION_ID = 0;
-
-        // 
https://www.mediawiki.org/wiki/Manual:Edit_token#The_edit_token_suffix
-        private static final String ANONYMOUS_TOKEN = "+\\";
-
-        private static final String DEFAULT_SUMMARY = "";
-
-        public static EditingResult execute(PageTitle title, String 
sectionWikitext) {
-            return execute(title, sectionWikitext, DEFAULT_SECTION_ID);
-        }
-
-        public static EditingResult execute(PageTitle title, String 
sectionWikitext, int sectionId) {
-            return execute(title, sectionWikitext, sectionId, ANONYMOUS_TOKEN);
-        }
-
-        public static EditingResult execute(PageTitle title, String 
sectionWikitext, int sectionId,
-                                            String token) {
-            Subject subject = new Subject(title, sectionWikitext, sectionId, 
token);
-            subject.execute();
-            return subject.await();
-        }
-
-        @NonNull private final TestLatch latch = new TestLatch();
-        private EditingResult result;
-
-        Subject(PageTitle title, String sectionWikitext, int sectionId, String 
token) {
-            super(getTargetContext(), title, sectionWikitext, sectionId, 
token, DEFAULT_SUMMARY,
-                    false);
-        }
-
-        @Override
-        public void onFinish(EditingResult result) {
-            super.onFinish(result);
-            this.result = result;
-            latch.countDown();
-        }
-
-        public EditingResult await() {
-            latch.await();
-            return result;
-        }
-    }
-}
diff --git 
a/app/src/androidTest/java/org/wikipedia/test/FetchSectionWikitextTaskTests.java
 
b/app/src/androidTest/java/org/wikipedia/test/FetchSectionWikitextTaskTests.java
deleted file mode 100644
index 3e2a0e6..0000000
--- 
a/app/src/androidTest/java/org/wikipedia/test/FetchSectionWikitextTaskTests.java
+++ /dev/null
@@ -1,41 +0,0 @@
-
-package org.wikipedia.test;
-
-import android.support.test.filters.SmallTest;
-import android.test.ActivityUnitTestCase;
-
-import org.wikipedia.dataclient.WikiSite;
-import org.wikipedia.page.PageTitle;
-import org.wikipedia.editing.FetchSectionWikitextTask;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@SmallTest
-public class FetchSectionWikitextTaskTests extends 
ActivityUnitTestCase<TestDummyActivity> {
-    private static final int TASK_COMPLETION_TIMEOUT = 20_000;
-
-    public FetchSectionWikitextTaskTests() {
-        super(TestDummyActivity.class);
-    }
-
-    public void testPageFetch() throws Throwable {
-        final CountDownLatch completionLatch = new CountDownLatch(1);
-        runTestOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                PageTitle title = new PageTitle(null, 
"Test_page_for_app_testing/Section1", WikiSite.forLanguageCode("test"));
-                new 
FetchSectionWikitextTask(getInstrumentation().getTargetContext(), title, 2) {
-                    @Override
-                    public void onFinish(String result) {
-                        assertNotNull(result);
-                        assertEquals(result, "=== Section1.2 ===\nThis is a 
subsection");
-                        completionLatch.countDown();
-                    }
-                }.execute();
-            }
-        });
-        assertTrue(completionLatch.await(TASK_COMPLETION_TIMEOUT, 
TimeUnit.MILLISECONDS));
-    }
-}
-
diff --git a/app/src/main/java/org/wikipedia/captcha/Captcha.java 
b/app/src/main/java/org/wikipedia/captcha/Captcha.java
new file mode 100644
index 0000000..805bd9c
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/captcha/Captcha.java
@@ -0,0 +1,22 @@
+package org.wikipedia.captcha;
+
+import android.support.annotation.NonNull;
+
+public class Captcha {
+    @SuppressWarnings("unused,NullableProblems") @NonNull private 
FancyCaptchaReload fancycaptchareload;
+    @NonNull protected FancyCaptchaReload content() {
+        return fancycaptchareload;
+    }
+
+    protected class FancyCaptchaReload {
+        @SuppressWarnings("unused,NullableProblems") @NonNull private String 
index;
+        @NonNull protected String index() {
+            return index;
+        }
+    }
+
+    public interface Callback {
+        void success(@NonNull CaptchaResult result);
+        void failure(@NonNull Throwable caught);
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/captcha/CaptchaClient.java 
b/app/src/main/java/org/wikipedia/captcha/CaptchaClient.java
new file mode 100644
index 0000000..67dfc16
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/captcha/CaptchaClient.java
@@ -0,0 +1,45 @@
+package org.wikipedia.captcha;
+
+
+import android.support.annotation.NonNull;
+
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.dataclient.retrofit.MwCachedService;
+import org.wikipedia.dataclient.retrofit.RetrofitException;
+import org.wikipedia.server.restbase.RbPageEndpointsCache;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.http.GET;
+
+public class CaptchaClient {
+    @NonNull private final MwCachedService<Service> cachedService = new 
MwCachedService<>(Service.class);
+    @NonNull private final Retrofit retrofit = 
RbPageEndpointsCache.INSTANCE.getRetrofit();
+
+    public void refreshCaptcha(@NonNull final WikiSite wiki, @NonNull final 
Captcha.Callback cb) {
+        Call<Captcha> call = cachedService.service(wiki).refreshCaptcha();
+        call.enqueue(new Callback<Captcha>() {
+            @Override
+            public void onResponse(Call<Captcha> call, Response<Captcha> 
response) {
+                if (response.isSuccessful()) {
+                    cb.success(new 
CaptchaResult(response.body().content().index()));
+                } else {
+                    cb.failure(RetrofitException.httpError(response, 
retrofit));
+                }
+            }
+
+            @Override
+            public void onFailure(Call<Captcha> call, Throwable t) {
+                cb.failure(t);
+            }
+        });
+    }
+
+    private interface Service {
+        /* Get a fresh Captcha ID. */
+        @GET("w/api.php?action=fancycaptchareload&format=json")
+        Call<Captcha> refreshCaptcha();
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/editing/CaptchaHandler.java 
b/app/src/main/java/org/wikipedia/captcha/CaptchaHandler.java
similarity index 89%
rename from app/src/main/java/org/wikipedia/editing/CaptchaHandler.java
rename to app/src/main/java/org/wikipedia/captcha/CaptchaHandler.java
index 3f07ca0..9f0db82 100644
--- a/app/src/main/java/org/wikipedia/editing/CaptchaHandler.java
+++ b/app/src/main/java/org/wikipedia/captcha/CaptchaHandler.java
@@ -1,4 +1,4 @@
-package org.wikipedia.editing;
+package org.wikipedia.captcha;
 
 import android.app.Activity;
 import android.app.ProgressDialog;
@@ -59,26 +59,24 @@
         captchaImage.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                new RefreshCaptchaTask(activity, wiki) {
-                    @Override
-                    public void onBeforeExecute() {
-                        captchaProgress.setVisibility(View.VISIBLE);
-                    }
+                captchaProgress.setVisibility(View.VISIBLE);
 
+                CaptchaClient apiService = new CaptchaClient();
+                apiService.refreshCaptcha(wiki, new Captcha.Callback() {
                     @Override
-                    public void onFinish(@NonNull CaptchaResult result) {
+                    public void success(@NonNull CaptchaResult result) {
                         captchaResult = result;
                         captchaProgress.setVisibility(View.GONE);
                         handleCaptcha(true);
                     }
 
                     @Override
-                    public void onCatch(Throwable caught) {
+                    public void failure(@NonNull Throwable caught) {
                         cancelCaptcha();
                         captchaProgress.setVisibility(View.GONE);
                         FeedbackUtil.showError(activity, caught);
                     }
-                }.execute();
+                });
             }
         });
     }
@@ -88,6 +86,22 @@
         return token;
     }
 
+    @Nullable
+    public String captchaId() {
+        if (captchaResult != null) {
+            return captchaResult.getCaptchaId();
+        }
+        return null;
+    }
+
+    @Nullable
+    public String captchaWord() {
+        if (captchaText != null) {
+            return captchaText.getText().toString();
+        }
+        return null;
+    }
+
     public void restoreState(Bundle savedInstanceState) {
         if (savedInstanceState != null
                 && savedInstanceState.containsKey("token")
diff --git a/app/src/main/java/org/wikipedia/editing/CaptchaResult.java 
b/app/src/main/java/org/wikipedia/captcha/CaptchaResult.java
similarity index 93%
rename from app/src/main/java/org/wikipedia/editing/CaptchaResult.java
rename to app/src/main/java/org/wikipedia/captcha/CaptchaResult.java
index 9d82d13..9b175e4 100644
--- a/app/src/main/java/org/wikipedia/editing/CaptchaResult.java
+++ b/app/src/main/java/org/wikipedia/captcha/CaptchaResult.java
@@ -1,8 +1,9 @@
-package org.wikipedia.editing;
+package org.wikipedia.captcha;
 
 import android.os.Parcel;
 import android.os.Parcelable;
 import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.editing.EditingResult;
 
 // Handles only Image Captchas
 public class CaptchaResult extends EditingResult {
diff --git a/app/src/main/java/org/wikipedia/editing/Edit.java 
b/app/src/main/java/org/wikipedia/editing/Edit.java
new file mode 100644
index 0000000..6a84a66
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/editing/Edit.java
@@ -0,0 +1,58 @@
+package org.wikipedia.editing;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+public class Edit {
+    @SuppressWarnings("unused,NullableProblems") @NonNull private Result edit;
+    @NonNull protected Result edit() {
+        return edit;
+    }
+
+    protected class Result {
+        @Nullable private String result;
+        @Nullable protected String status() {
+            return result;
+        }
+
+        private int newrevid;
+        protected int newRevId() {
+            return newrevid;
+        }
+
+        @Nullable private Captcha captcha;
+        @Nullable protected Captcha captcha() {
+            return captcha;
+        }
+
+        @Nullable private String code;
+        @Nullable protected String code() {
+            return code;
+        }
+
+        protected boolean hasErrorCode() {
+            return code != null;
+        }
+
+        @Nullable private String spamblacklist;
+        @Nullable protected String spamblacklist() {
+            return spamblacklist;
+        }
+
+        protected boolean hasSpamBlacklistResponse() {
+            return spamblacklist != null;
+        }
+    }
+
+    protected class Captcha {
+        @SuppressWarnings("unused,NullableProblems") @NonNull private String 
id;
+        @NonNull protected String id() {
+            return id;
+        }
+    }
+
+    public interface Callback {
+        void success(@NonNull EditingResult result);
+        void failure(@NonNull Throwable caught);
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/editing/EditClient.java 
b/app/src/main/java/org/wikipedia/editing/EditClient.java
new file mode 100644
index 0000000..00d57ac
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/editing/EditClient.java
@@ -0,0 +1,197 @@
+package org.wikipedia.editing;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.dataclient.retrofit.MwCachedService;
+import org.wikipedia.dataclient.retrofit.RetrofitException;
+import org.wikipedia.json.GsonMarshaller;
+import org.wikipedia.json.GsonUnmarshaller;
+import org.wikipedia.page.PageTitle;
+import org.wikipedia.server.mwapi.ApiResponsePage;
+import org.wikipedia.server.restbase.RbPageEndpointsCache;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.http.Field;
+import retrofit2.http.FormUrlEncoded;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.Query;
+
+public class EditClient {
+    @NonNull private final MwCachedService<Service> cachedService = new 
MwCachedService<>(Service.class);
+    @NonNull private final Retrofit retrofit = 
RbPageEndpointsCache.INSTANCE.getRetrofit();
+
+    public void editToken(@NonNull final WikiSite wiki, @NonNull final 
EditToken.Callback cb) {
+        Call<EditToken> call = cachedService.service(wiki).editToken();
+        call.enqueue(new Callback<EditToken>() {
+            @Override
+            public void onResponse(Call<EditToken> call, Response<EditToken> 
response) {
+                if (response.isSuccessful()) {
+                    cb.success(response.body().query().tokens().token());
+                } else {
+                    cb.failure(RetrofitException.httpError(response, 
retrofit));
+                }
+            }
+
+            @Override
+            public void onFailure(Call<EditToken> call, Throwable t) {
+                cb.failure(t);
+            }
+        });
+    }
+
+    @NonNull
+    public String editTokenSync(@NonNull final WikiSite wiki) throws 
IOException {
+        Call<EditToken> call = cachedService.service(wiki).editToken();
+        return call.execute().body().query().tokens().token();
+    }
+
+    @SuppressWarnings("checkstyle:parameternumber")
+    public void edit(@NonNull final WikiSite wiki, @NonNull final PageTitle 
title,
+                     final int section, @NonNull final String text, @NonNull 
final String token,
+                     @NonNull final String summary, final boolean loggedIn,
+                     @Nullable final String captchaId, @Nullable final String 
captchaWord,
+                     @NonNull final Edit.Callback cb) {
+        Call<Edit> call = cachedService.service(wiki).edit("edit", "json", 
title.getPrefixedText(),
+                section, text, token, summary, loggedIn ? "user" : null, 
captchaId, captchaWord);
+        call.enqueue(new Callback<Edit>() {
+            @Override
+            public void onResponse(Call<Edit> call, Response<Edit> response) {
+                if (response.isSuccessful()) {
+                    Edit.Result result = response.body().edit();
+                    if ("Success".equals(result.status())) {
+                        try {
+                            // TODO: remove when the server reflects the 
updated page content
+                            // immediately after submitting the edit, instead 
of a short while after.
+                            Thread.sleep(TimeUnit.SECONDS.toMillis(2));
+                            cb.success(new 
SuccessEditResult(result.newRevId()));
+                        } catch (InterruptedException e) {
+                            cb.failure(e);
+                        }
+                    } else if (result.hasErrorCode()) {
+                        try {
+                            JSONObject json = new 
JSONObject(GsonMarshaller.marshal(result));
+                            cb.success(new AbuseFilterEditResult(json));
+                        } catch (JSONException e) {
+                            cb.failure(e);
+                        }
+                    } else if (result.hasSpamBlacklistResponse()) {
+                        cb.success(new 
SpamBlacklistEditResult(result.spamblacklist()));
+                    } else {
+                        cb.failure(new RuntimeException("Received unrecognized 
edit response"));
+                    }
+                }
+            }
+
+            @Override
+            public void onFailure(Call<Edit> call, Throwable t) {
+
+            }
+        });
+    }
+
+    public void wikitext(@NonNull final WikiSite wiki, @NonNull final 
PageTitle title,
+                         final int sectionID, @NonNull final Wikitext.Callback 
cb) {
+        Call<Wikitext> call = 
cachedService.service(wiki).wikitext(title.getPrefixedText(), sectionID);
+        call.enqueue(new Callback<Wikitext>() {
+            @Override
+            public void onResponse(Call<Wikitext> call, Response<Wikitext> 
response) {
+                if (response.isSuccessful()) {
+                    JsonObject pages = response.body().query().pages();
+                    Set<Map.Entry<String, JsonElement>> entries = 
pages.entrySet();
+                    if (!entries.isEmpty()) {
+                        Iterator<Map.Entry<String, JsonElement>> i = 
entries.iterator();
+                        Map.Entry<String, JsonElement> entry = i.next();
+                        String value = 
entry.getValue().getAsJsonObject().toString();
+                        ApiResponsePage page = 
GsonUnmarshaller.unmarshal(ApiResponsePage.class, value);
+                        ArrayList<ApiResponsePage.Revision> revisions = new 
ArrayList(page.revisions());
+                        cb.success(revisions.get(0).content());
+                    }
+                } else {
+                    cb.failure(RetrofitException.httpError(response, 
retrofit));
+                }
+            }
+
+            @Override
+            public void onFailure(Call<Wikitext> call, Throwable t) {
+                cb.failure(t);
+            }
+        });
+    }
+
+
+    public void previewEdit(@NonNull final WikiSite wiki, @NonNull final 
PageTitle title,
+                            @NonNull final String wikitext, @NonNull final 
EditPreview.Callback cb) {
+        Call<EditPreview> call = 
cachedService.service(wiki).previewEdit("parse", "json", true,
+                true, true, "text", title.getPrefixedText(), wikitext);
+        call.enqueue(new Callback<EditPreview>() {
+            @Override
+            public void onResponse(Call<EditPreview> call, 
Response<EditPreview> response) {
+                if (response.isSuccessful()) {
+                    cb.success(response.body().parse().text().result());
+                } else {
+                    cb.failure(RetrofitException.httpError(response, 
retrofit));
+                }
+            }
+
+            @Override
+            public void onFailure(Call<EditPreview> call, Throwable t) {
+                cb.failure(t);
+            }
+        });
+    }
+
+    private interface Service {
+        @GET("w/api.php?action=query&format=json&meta=tokens&type=csrf")
+        Call<EditToken> editToken();
+
+        @FormUrlEncoded
+        @POST("w/api.php")
+        @SuppressWarnings("checkstyle:parameternumber")
+        Call<Edit> edit(@NonNull @Field("action") String action,
+                        @NonNull @Field("format") String format,
+                        @NonNull @Field("title") String title,
+                        @Field("section") int section,
+                        @NonNull @Field("text") String text,
+                        @NonNull @Field("token") String token,
+                        @NonNull @Field("summary") String summary,
+                        @Nullable @Field("assert") String user,
+                        @Nullable @Field("captchaId") String captchaId,
+                        @Nullable @Field("captchaWord") String captchaWord);
+
+
+        
@GET("w/api.php?action=query&format=json&prop=revisions&rvprop=content&rvlimit=1")
+        Call<Wikitext> wikitext(@NonNull @Query("titles") String title,
+                                @Query("rvsection") int section);
+
+        @FormUrlEncoded
+        @POST("w/api.php")
+        @SuppressWarnings("checkstyle:parameternumber")
+        Call<EditPreview> previewEdit(@NonNull @Field("action") String action,
+                                      @NonNull @Field("format") String format,
+                                      @Field("sectionpreview") boolean 
sectionPreview,
+                                      @Field("pst") boolean pst,
+                                      @Field("mobileformat") boolean 
mobileformat,
+                                      @NonNull @Field("prop") String prop,
+                                      @NonNull @Field("title") String title,
+                                      @NonNull @Field("text") String text);
+
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/editing/EditPreview.java 
b/app/src/main/java/org/wikipedia/editing/EditPreview.java
new file mode 100644
index 0000000..158a261
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/editing/EditPreview.java
@@ -0,0 +1,31 @@
+package org.wikipedia.editing;
+
+import android.support.annotation.NonNull;
+
+import com.google.gson.annotations.SerializedName;
+
+public class EditPreview {
+    @SuppressWarnings("unused,NullableProblems") private Parse parse;
+    @NonNull protected Parse parse() {
+        return parse;
+    }
+
+    protected class Parse {
+        @SuppressWarnings("unused,NullableProblems") private Text text;
+        @NonNull protected Text text() {
+            return text;
+        }
+    }
+
+    protected class Text {
+        @SuppressWarnings("unused,NullableProblems") @SerializedName("*") 
private String result;
+        @NonNull protected String result() {
+            return result;
+        }
+    }
+
+    public interface Callback {
+        void success(@NonNull String preview);
+        void failure(@NonNull Throwable caught);
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/editing/EditPreviewFragment.java 
b/app/src/main/java/org/wikipedia/editing/EditPreviewFragment.java
index 4681b1e..b335961 100644
--- a/app/src/main/java/org/wikipedia/editing/EditPreviewFragment.java
+++ b/app/src/main/java/org/wikipedia/editing/EditPreviewFragment.java
@@ -11,7 +11,6 @@
 import android.support.v4.app.Fragment;
 import android.support.v7.app.AlertDialog;
 import android.util.DisplayMetrics;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -29,6 +28,7 @@
 import org.wikipedia.page.PageTitle;
 import org.wikipedia.util.L10nUtil;
 import org.wikipedia.util.UriUtil;
+import org.wikipedia.util.log.L;
 import org.wikipedia.views.ObservableWebView;
 import org.wikipedia.bridge.CommunicationBridge;
 import org.wikipedia.editing.summaries.EditSummaryTag;
@@ -291,27 +291,24 @@
      */
     public void showPreview(final PageTitle title, final String wikiText) {
         hideSoftKeyboard(getActivity());
+        progressDialog.show();
 
-        new EditPreviewTask(WikipediaApp.getInstance(), wikiText, title) {
+        new 
EditClient().previewEdit(parentActivity.getPageTitle().getWikiSite(), title, 
wikiText,
+                new EditPreview.Callback() {
             @Override
-            public void onBeforeExecute() {
-                progressDialog.show();
-            }
-
-            @Override
-            public void onFinish(String result) {
+            public void success(@NonNull String preview) {
                 if (!progressDialog.isShowing()) {
                     // no longer attached to activity!
                     return;
                 }
-                displayPreview(result);
-                previewHTML = result;
+                displayPreview(preview);
+                previewHTML = preview;
                 parentActivity.supportInvalidateOptionsMenu();
                 progressDialog.dismiss();
             }
 
             @Override
-            public void onCatch(Throwable caught) {
+            public void failure(@NonNull Throwable caught) {
                 if (!progressDialog.isShowing()) {
                     // no longer attached to activity!
                     return;
@@ -321,25 +318,27 @@
                 if (!(caught instanceof ApiException)) {
                     throw new RuntimeException(caught);
                 }
-                Log.d("Wikipedia", "Caught " + caught.toString());
+                L.d("Caught " + caught.toString());
                 final AlertDialog retryDialog = new 
AlertDialog.Builder(getActivity())
                         .setMessage(R.string.error_network_error)
-                        
.setPositiveButton(R.string.dialog_message_edit_failed_retry, new 
DialogInterface.OnClickListener() {
-                            @Override
-                            public void onClick(DialogInterface dialog, int 
which) {
-                                showPreview(title, wikiText);
-                                dialog.dismiss();
-                            }
-                        })
-                        
.setNegativeButton(R.string.dialog_message_edit_failed_cancel, new 
DialogInterface.OnClickListener() {
-                            @Override
-                            public void onClick(DialogInterface dialog, int 
which) {
-                                dialog.dismiss();
-                            }
-                        }).create();
+                        
.setPositiveButton(R.string.dialog_message_edit_failed_retry,
+                                new DialogInterface.OnClickListener() {
+                                    @Override
+                                    public void onClick(DialogInterface 
dialog, int which) {
+                                        showPreview(title, wikiText);
+                                        dialog.dismiss();
+                                    }
+                                })
+                        
.setNegativeButton(R.string.dialog_message_edit_failed_cancel,
+                                new DialogInterface.OnClickListener() {
+                                    @Override
+                                    public void onClick(DialogInterface 
dialog, int which) {
+                                        dialog.dismiss();
+                                    }
+                                }).create();
                 retryDialog.show();
             }
-        }.execute();
+        });
     }
 
     /**
diff --git a/app/src/main/java/org/wikipedia/editing/EditPreviewTask.java 
b/app/src/main/java/org/wikipedia/editing/EditPreviewTask.java
deleted file mode 100644
index cef89d3..0000000
--- a/app/src/main/java/org/wikipedia/editing/EditPreviewTask.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.wikipedia.editing;
-
-import android.support.annotation.NonNull;
-
-import org.mediawiki.api.json.Api;
-import org.mediawiki.api.json.ApiException;
-import org.mediawiki.api.json.ApiResult;
-import org.mediawiki.api.json.RequestBuilder;
-import org.wikipedia.dataclient.ApiTask;
-import org.wikipedia.WikipediaApp;
-import org.wikipedia.page.PageTitle;
-
-public class EditPreviewTask extends ApiTask<String> {
-    private final String wikiText;
-    @NonNull private final PageTitle title;
-
-    public EditPreviewTask(@NonNull WikipediaApp app, String wikiText, 
@NonNull PageTitle title) {
-        super(app.getAPIForSite(title.getWikiSite()));
-        this.wikiText = wikiText;
-        this.title = title;
-    }
-
-    @Override
-    public RequestBuilder buildRequest(Api api) {
-        return api.action("parse")
-                .param("sectionpreview", "true")
-                .param("pst", "true")
-                .param("mobileformat", "true")
-                .param("prop", "text")
-                .param("title", title.getPrefixedText())
-                .param("text", wikiText);
-    }
-
-    @Override
-    protected ApiResult makeRequest(RequestBuilder builder) throws 
ApiException {
-        return builder.post();
-    }
-
-    @Override
-    public String processResult(ApiResult result) throws Throwable {
-        return 
result.asObject().optJSONObject("parse").optJSONObject("text").optString("*");
-    }
-}
diff --git a/app/src/main/java/org/wikipedia/editing/EditSectionActivity.java 
b/app/src/main/java/org/wikipedia/editing/EditSectionActivity.java
index 7227d6c..16855fa 100644
--- a/app/src/main/java/org/wikipedia/editing/EditSectionActivity.java
+++ b/app/src/main/java/org/wikipedia/editing/EditSectionActivity.java
@@ -8,6 +8,9 @@
 import org.wikipedia.activity.ThemedActionBarActivity;
 import org.wikipedia.analytics.EditFunnel;
 import org.wikipedia.analytics.LoginFunnel;
+import org.wikipedia.auth.AccountUtil;
+import org.wikipedia.captcha.CaptchaHandler;
+import org.wikipedia.captcha.CaptchaResult;
 import org.wikipedia.editing.richtext.SyntaxHighlighter;
 import org.wikipedia.editing.summaries.EditSummaryFragment;
 import org.wikipedia.login.LoginActivity;
@@ -21,10 +24,10 @@
 import org.wikipedia.util.StringUtil;
 import org.wikipedia.util.log.L;
 
-import org.mediawiki.api.json.Api;
 import org.mediawiki.api.json.ApiException;
-import org.mediawiki.api.json.RequestBuilder;
 
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
 import android.app.ProgressDialog;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -62,6 +65,8 @@
     public static final String EXTRA_HIGHLIGHT_TEXT = 
"org.wikipedia.edit_section.highlight";
 
     private WikipediaApp app;
+    private EditClient apiService;
+    private LoginClient loginService;
 
     private PageTitle title;
     public PageTitle getPageTitle() {
@@ -111,6 +116,8 @@
         }
 
         app = (WikipediaApp)getApplicationContext();
+        apiService = new EditClient();
+        loginService = new LoginClient();
 
         title = getIntent().getParcelableExtra(EXTRA_TITLE);
         sectionID = getIntent().getIntExtra(EXTRA_SECTION_ID, 0);
@@ -273,79 +280,73 @@
                 // Summaries are plaintext, so remove any HTML that's made its 
way into the summary
                 summaryText = StringUtil.fromHtml(summaryText).toString();
 
-                new EditTask(EditSectionActivity.this, title, 
sectionText.getText().toString(),
-                        sectionID, token, summaryText, User.isLoggedIn()) {
-                    @Override
-                    public void onBeforeExecute() {
-                        if (!isFinishing()) {
-                            progressDialog.show();
-                        }
-                    }
+                if (!isFinishing()) {
+                    progressDialog.show();
+                }
 
-                    @Override
-                    public RequestBuilder buildRequest(Api api) {
-                        return 
captchaHandler.populateBuilder(super.buildRequest(api));
-                    }
+                apiService.edit(title.getWikiSite(), title, sectionID,
+                        sectionText.getText().toString(), token, summaryText, 
User.isLoggedIn(),
+                        captchaHandler.isActive() ? captchaHandler.captchaId() 
: "null",
+                        captchaHandler.isActive() ? 
captchaHandler.captchaWord() : "null",
+                        new Edit.Callback() {
+                            @Override
+                            public void success(@NonNull EditingResult result) 
{
+                                if (isFinishing() || 
!progressDialog.isShowing()) {
+                                    // no longer attached to activity!
+                                    return;
+                                }
+                                if (result instanceof SuccessEditResult) {
+                                    funnel.logSaved(((SuccessEditResult) 
result).getRevID());
+                                    progressDialog.dismiss();
 
-                    @Override
-                    public void onCatch(Throwable caught) {
-                        if (isFinishing() || !progressDialog.isShowing()) {
-                            // no longer attached to activity!
-                            return;
-                        }
-                        if (caught instanceof ApiException) {
-                            // This is a fairly standard editing exception. 
Handle it appropriately.
-                            handleEditingException((ApiException) caught);
-                        } else {
-                            // If it's not an API exception, we have no idea 
what's wrong.
-                            // Show the user a generic error message.
-                            L.w("Caught " + caught.toString());
-                            showRetryDialog();
-                        }
-                    }
-
-                    @Override
-                    public void onFinish(EditingResult result) {
-                        if (isFinishing() || !progressDialog.isShowing()) {
-                            // no longer attached to activity!
-                            return;
-                        }
-                        if (result instanceof SuccessEditResult) {
-                            funnel.logSaved(((SuccessEditResult) 
result).getRevID());
-                            progressDialog.dismiss();
-
-                            //Build intent that includes the section we were 
editing, so we can scroll to it later
-                            Intent data = new Intent();
-                            data.putExtra(EXTRA_SECTION_ID, sectionID);
-                            setResult(EditHandler.RESULT_REFRESH_PAGE, data);
-                            hideSoftKeyboard(EditSectionActivity.this);
-                            finish();
-                        } else if (result instanceof CaptchaResult) {
-                            if (captchaHandler.isActive()) {
-                                // Captcha entry failed!
-                                funnel.logCaptchaFailure();
+                                    //Build intent that includes the section 
we were editing, so we can scroll to it later
+                                    Intent data = new Intent();
+                                    data.putExtra(EXTRA_SECTION_ID, sectionID);
+                                    setResult(EditHandler.RESULT_REFRESH_PAGE, 
data);
+                                    hideSoftKeyboard(EditSectionActivity.this);
+                                    finish();
+                                } else if (result instanceof CaptchaResult) {
+                                    if (captchaHandler.isActive()) {
+                                        // Captcha entry failed!
+                                        funnel.logCaptchaFailure();
+                                    }
+                                    captchaHandler.handleCaptcha(null, 
(CaptchaResult) result);
+                                    funnel.logCaptchaShown();
+                                } else if (result instanceof 
AbuseFilterEditResult) {
+                                    abusefilterEditResult = 
(AbuseFilterEditResult) result;
+                                    handleAbuseFilter();
+                                    if (abusefilterEditResult.getType() == 
AbuseFilterEditResult.TYPE_ERROR) {
+                                        editPreviewFragment.hide();
+                                    }
+                                } else if (result instanceof 
SpamBlacklistEditResult) {
+                                    
FeedbackUtil.showMessage(EditSectionActivity.this,
+                                            
R.string.editing_error_spamblacklist);
+                                    progressDialog.dismiss();
+                                    editPreviewFragment.hide();
+                                } else {
+                                    funnel.logError(result.getResult());
+                                    // Expand to do everything.
+                                    failure(new Throwable());
+                                }
                             }
-                            captchaHandler.handleCaptcha(null, (CaptchaResult) 
result);
-                            funnel.logCaptchaShown();
-                        } else if (result instanceof AbuseFilterEditResult) {
-                            abusefilterEditResult = (AbuseFilterEditResult) 
result;
-                            handleAbuseFilter();
-                            if (abusefilterEditResult.getType() == 
AbuseFilterEditResult.TYPE_ERROR) {
-                                editPreviewFragment.hide();
-                            }
-                        } else if (result instanceof SpamBlacklistEditResult) {
-                            FeedbackUtil.showMessage(EditSectionActivity.this,
-                                    R.string.editing_error_spamblacklist);
-                            progressDialog.dismiss();
-                            editPreviewFragment.hide();
-                        } else {
-                            funnel.logError(result.getResult());
-                            // Expand to do everything.
-                            onCatch(null);
-                        }
 
-                    }
-                }.execute();
+                            @Override
+                            public void failure(@NonNull Throwable caught) {
+                                if (isFinishing() || 
!progressDialog.isShowing()) {
+                                    // no longer attached to activity!
+                                    return;
+                                }
+                                if (caught instanceof ApiException) {
+                                    // This is a fairly standard editing 
exception. Handle it appropriately.
+                                    handleEditingException((ApiException) 
caught);
+                                } else {
+                                    // If it's not an API exception, we have 
no idea what's wrong.
+                                    // Show the user a generic error message.
+                                    L.w("Caught " + caught.toString());
+                                    showRetryDialog();
+                                }
+                            }
+                        });
             }
 
             @Override
@@ -439,34 +440,55 @@
     }
 
     private void doLoginAndSave(final User user) {
-        new LoginClient().request(WikipediaApp.getInstance().getWikiSite(),
-                user.getUsername(),
-                user.getPassword(),
-                new LoginClient.LoginCallback() {
-                    @Override
-                    public void success(@NonNull LoginResult result) {
-                        if (result.pass()) {
-                            doSave();
-                        } else {
-                            onLoginError();
-                        }
-                    }
+        final String username = user.getUsername();
+        final String password = user.getPassword();
+        final LoginFunnel loginFunnel = new 
LoginFunnel(WikipediaApp.getInstance());
+        loginService.request(title.getWikiSite(), username, password, new 
LoginClient.LoginCallback() {
+            @Override
+            public void success(@NonNull LoginResult result) {
+                if (result.pass() && result.getUser() != null) {
+                    User.setUser(result.getUser());
 
-                    @Override
-                    public void error(@NonNull Throwable caught) {
-                        onLoginError();
+                    if (!progressDialog.isShowing()) {
+                        // no longer attached to activity!
+                        return;
                     }
+                    progressDialog.dismiss();
 
-                    private void onLoginError() {
-                        if (!progressDialog.isShowing()) {
-                            // no longer attached to activity!
-                            return;
-                        }
-                        progressDialog.dismiss();
-                        ViewAnimations.crossFade(sectionText, sectionError);
-                        sectionError.setVisibility(View.VISIBLE);
-                    }
-                });
+                    Bundle extras = getIntent().getExtras();
+                    AccountAuthenticatorResponse response = extras == null
+                            ? null
+                            : 
extras.<AccountAuthenticatorResponse>getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
+                    AccountUtil.createAccount(response, username, password);
+
+                    setResult(LoginActivity.RESULT_LOGIN_SUCCESS);
+                    loginFunnel.logSuccess();
+
+                    doSave();
+                } else {
+                    progressDialog.dismiss();
+                    FeedbackUtil.showMessage(EditSectionActivity.this, 
result.getMessage());
+                    ViewAnimations.crossFade(sectionText, sectionError);
+                    sectionError.setVisibility(View.VISIBLE);
+                }
+            }
+
+            @Override
+            public void error(@NonNull Throwable t) {
+                handleError(t);
+            }
+        });
+    }
+
+    private void handleError(Throwable t) {
+        if (!progressDialog.isShowing()) {
+            // no longer attached to activity!
+            return;
+        }
+        progressDialog.dismiss();
+        FeedbackUtil.showError(EditSectionActivity.this, t);
+        funnel.logError(t.getMessage());
+        L.e("Login failed with result " + t.getMessage());
     }
 
     private void handleAbuseFilter() {
@@ -610,22 +632,22 @@
 
     private void fetchSectionText() {
         if (sectionWikitext == null) {
-            new FetchSectionWikitextTask(this, title, sectionID) {
+            apiService.wikitext(title.getWikiSite(), title, sectionID, new 
Wikitext.Callback() {
                 @Override
-                public void onFinish(String result) {
-                    sectionWikitext = result;
+                public void success(@NonNull String wikitext) {
+                    sectionWikitext = wikitext;
                     displaySectionText();
                 }
 
                 @Override
-                public void onCatch(Throwable caught) {
+                public void failure(@NonNull Throwable throwable) {
                     ViewAnimations.crossFade(sectionProgress, sectionError);
                     // Not sure why this is required, but without it tapping 
retry hides langLinksError
                     // FIXME: INVESTIGATE WHY THIS HAPPENS!
                     // Also happens in {@link PageFragment}
                     sectionError.setVisibility(View.VISIBLE);
                 }
-            }.execute();
+            });
         } else {
             displaySectionText();
         }
diff --git a/app/src/main/java/org/wikipedia/editing/EditTask.java 
b/app/src/main/java/org/wikipedia/editing/EditTask.java
deleted file mode 100644
index 27a444f..0000000
--- a/app/src/main/java/org/wikipedia/editing/EditTask.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package org.wikipedia.editing;
-
-import android.content.Context;
-import org.json.JSONObject;
-import org.mediawiki.api.json.Api;
-import org.mediawiki.api.json.ApiException;
-import org.mediawiki.api.json.ApiResult;
-import org.mediawiki.api.json.RequestBuilder;
-import org.wikipedia.dataclient.ApiTask;
-import org.wikipedia.page.PageTitle;
-import org.wikipedia.WikipediaApp;
-
-import java.util.concurrent.TimeUnit;
-
-public class EditTask extends ApiTask<EditingResult> {
-    private final PageTitle title;
-    private final String sectionWikitext;
-    private final int sectionID;
-    private final String summary;
-    private final String editToken;
-    private final boolean loggedIn;
-
-    public EditTask(Context context, PageTitle title, String sectionWikitext, 
int sectionID,
-                      String editToken, String summary, boolean loggedIn) {
-        
super(((WikipediaApp)context.getApplicationContext()).getAPIForSite(title.getWikiSite()));
-        this.title = title;
-        this.sectionWikitext = sectionWikitext;
-        this.sectionID = sectionID;
-        this.editToken = editToken;
-        this.summary = summary;
-        this.loggedIn = loggedIn;
-    }
-
-    @Override
-    public RequestBuilder buildRequest(Api api) {
-        RequestBuilder req = api.action("edit")
-                .param("title", title.getPrefixedText())
-                .param("section", String.valueOf(sectionID))
-                .param("text", sectionWikitext)
-                .param("token", editToken)
-                .param("summary", summary);
-        if (loggedIn) {
-            // if the app believes that the user is logged in, then make sure 
to send an "assert"
-            // parameter to the API, so that it will notify us if the user was 
actually logged
-            // out on another device, and we'll need to log back in behind the 
scenes.
-            req = req.param("assert", "user");
-        }
-        return req;
-    }
-
-    @Override
-    protected ApiResult makeRequest(RequestBuilder builder) throws 
ApiException {
-        return builder.post(); // Editing requires POST requests
-    }
-
-    @Override
-    public EditingResult processResult(ApiResult result) throws Throwable {
-        JSONObject resultJSON = result.asObject();
-        JSONObject edit = resultJSON.optJSONObject("edit");
-        String status = edit.optString("result");
-        if (status.equals("Success")) {
-
-            // TODO: remove when the server reflects the updated page content 
immediately
-            // after submitting the edit, instead of a short while after.
-            Thread.sleep(TimeUnit.SECONDS.toMillis(2));
-
-            return new SuccessEditResult(edit.optInt("newrevid"));
-        } else if (status.equals("Failure")) {
-            if (edit.has("captcha")) {
-                return new CaptchaResult(
-                        edit.optJSONObject("captcha").optString("id")
-                );
-            }
-            if (edit.has("code")) {
-                return new AbuseFilterEditResult(edit);
-            }
-            if (edit.has("spamblacklist")) {
-                return new 
SpamBlacklistEditResult(edit.optString("spamblacklist"));
-            }
-        }
-        // Handle other type of return codes here
-        throw new RuntimeException("Failure to recognise edit status: " + 
resultJSON.toString());
-    }
-}
diff --git a/app/src/main/java/org/wikipedia/editing/EditToken.java 
b/app/src/main/java/org/wikipedia/editing/EditToken.java
new file mode 100644
index 0000000..f679480
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/editing/EditToken.java
@@ -0,0 +1,29 @@
+package org.wikipedia.editing;
+
+import android.support.annotation.NonNull;
+
+public class EditToken {
+    @SuppressWarnings("unused,NullableProblems") @NonNull private Query query;
+    @NonNull protected Query query() {
+        return query;
+    }
+
+    protected class Query {
+        @SuppressWarnings("unused,NullableProblems") @NonNull private Tokens 
tokens;
+        @NonNull protected Tokens tokens() {
+            return tokens;
+        }
+    }
+
+    protected class Tokens {
+        @SuppressWarnings("unused,NullableProblems") @NonNull private String 
csrftoken;
+        @NonNull protected String token() {
+            return csrftoken;
+        }
+    }
+
+    public interface Callback {
+        void success(@NonNull String token);
+        void failure(@NonNull Throwable caught);
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/editing/EditTokenStorage.java 
b/app/src/main/java/org/wikipedia/editing/EditTokenStorage.java
index 197285c..da4f397 100644
--- a/app/src/main/java/org/wikipedia/editing/EditTokenStorage.java
+++ b/app/src/main/java/org/wikipedia/editing/EditTokenStorage.java
@@ -50,18 +50,19 @@
             return;
         }
 
-        new FetchEditTokenTask(context, wiki) {
+        EditClient apiService = new EditClient();
+        apiService.editToken(wiki, new EditToken.Callback() {
             @Override
-            public void onFinish(String result) {
-                token(wiki, result);
-                callback.onTokenRetrieved(result);
+            public void success(@NonNull String token) {
+                token(wiki, token);
+                callback.onTokenRetrieved(token);
             }
 
             @Override
-            public void onCatch(Throwable caught) {
+            public void failure(@NonNull Throwable caught) {
                 callback.onTokenFailed(caught);
             }
-        }.execute();
+        });
     }
 
     public void clearEditTokenForDomain(String wiki) {
diff --git a/app/src/main/java/org/wikipedia/editing/FetchEditTokenTask.java 
b/app/src/main/java/org/wikipedia/editing/FetchEditTokenTask.java
deleted file mode 100644
index 159c763..0000000
--- a/app/src/main/java/org/wikipedia/editing/FetchEditTokenTask.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.wikipedia.editing;
-
-import android.content.Context;
-import org.mediawiki.api.json.Api;
-import org.mediawiki.api.json.ApiResult;
-import org.mediawiki.api.json.RequestBuilder;
-import org.wikipedia.dataclient.ApiTask;
-import org.wikipedia.dataclient.WikiSite;
-import org.wikipedia.WikipediaApp;
-
-public class FetchEditTokenTask extends ApiTask<String> {
-    public FetchEditTokenTask(Context context, WikiSite wiki) {
-        
super(((WikipediaApp)context.getApplicationContext()).getAPIForSite(wiki));
-    }
-
-    @Override
-    public RequestBuilder buildRequest(Api api) {
-        return api.action("tokens")
-                .param("type", "edit");
-    }
-
-    @Override
-    public String processResult(ApiResult result) throws Throwable {
-        return 
result.asObject().optJSONObject("tokens").optString("edittoken");
-    }
-}
diff --git 
a/app/src/main/java/org/wikipedia/editing/FetchSectionWikitextTask.java 
b/app/src/main/java/org/wikipedia/editing/FetchSectionWikitextTask.java
deleted file mode 100644
index 6309f39..0000000
--- a/app/src/main/java/org/wikipedia/editing/FetchSectionWikitextTask.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.wikipedia.editing;
-
-import android.content.Context;
-import org.json.JSONObject;
-import org.mediawiki.api.json.Api;
-import org.mediawiki.api.json.ApiResult;
-import org.mediawiki.api.json.RequestBuilder;
-import org.wikipedia.dataclient.ApiTask;
-import org.wikipedia.page.PageTitle;
-import org.wikipedia.WikipediaApp;
-
-public class FetchSectionWikitextTask extends ApiTask<String> {
-    private final PageTitle title;
-    private final int sectionID;
-
-    public FetchSectionWikitextTask(Context context, PageTitle title, int 
sectionID) {
-        
super(((WikipediaApp)context.getApplicationContext()).getAPIForSite(title.getWikiSite()));
-        this.title = title;
-        this.sectionID = sectionID;
-    }
-
-    @Override
-    public RequestBuilder buildRequest(Api api) {
-        return api.action("query")
-                .param("prop", "revisions")
-                .param("rvprop", "content")
-                .param("rvlimit", "1")
-                .param("titles", title.getPrefixedText())
-                .param("rvsection", String.valueOf(sectionID));
-    }
-
-    @Override
-    public String processResult(ApiResult result) throws Throwable {
-        JSONObject pagesJSON = result.asObject()
-                .optJSONObject("query")
-                .optJSONObject("pages");
-        String pageId = pagesJSON.keys().next();
-
-        JSONObject revisionJSON = 
pagesJSON.optJSONObject(pageId).optJSONArray("revisions").getJSONObject(0);
-
-        return revisionJSON.optString("*");
-    }
-}
diff --git a/app/src/main/java/org/wikipedia/editing/RefreshCaptchaTask.java 
b/app/src/main/java/org/wikipedia/editing/RefreshCaptchaTask.java
deleted file mode 100644
index 4f2dc67..0000000
--- a/app/src/main/java/org/wikipedia/editing/RefreshCaptchaTask.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.wikipedia.editing;
-
-import android.content.Context;
-import org.mediawiki.api.json.Api;
-import org.mediawiki.api.json.ApiResult;
-import org.mediawiki.api.json.RequestBuilder;
-import org.wikipedia.dataclient.ApiTask;
-import org.wikipedia.dataclient.WikiSite;
-import org.wikipedia.WikipediaApp;
-
-public class RefreshCaptchaTask extends ApiTask<CaptchaResult> {
-    public RefreshCaptchaTask(Context context, WikiSite wiki) {
-        
super(((WikipediaApp)context.getApplicationContext()).getAPIForSite(wiki));
-    }
-
-    @Override
-    public RequestBuilder buildRequest(Api api) {
-        return api.action("fancycaptchareload");
-    }
-
-    @Override
-    public CaptchaResult processResult(ApiResult result) throws Throwable {
-        return new CaptchaResult(
-                result.asObject()
-                        .optJSONObject("fancycaptchareload")
-                        .optString("index")
-        );
-    }
-}
diff --git a/app/src/main/java/org/wikipedia/editing/Wikitext.java 
b/app/src/main/java/org/wikipedia/editing/Wikitext.java
new file mode 100644
index 0000000..33e5fdd
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/editing/Wikitext.java
@@ -0,0 +1,24 @@
+package org.wikipedia.editing;
+
+import android.support.annotation.NonNull;
+
+import com.google.gson.JsonObject;
+
+public class Wikitext {
+    @SuppressWarnings("unused,NullableProblems") @NonNull private Query query;
+    @NonNull protected Query query() {
+        return query;
+    }
+
+    protected class Query {
+        @SuppressWarnings("unused,NullableProblems") @NonNull private 
JsonObject pages;
+        @NonNull protected JsonObject pages() {
+            return pages;
+        }
+    }
+
+    public interface Callback {
+        void success(@NonNull String wikitext);
+        void failure(@NonNull Throwable caught);
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/server/mwapi/ApiResponsePage.java 
b/app/src/main/java/org/wikipedia/server/mwapi/ApiResponsePage.java
new file mode 100644
index 0000000..8de3398
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/server/mwapi/ApiResponsePage.java
@@ -0,0 +1,45 @@
+package org.wikipedia.server.mwapi;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class ApiResponsePage {
+    @SuppressWarnings("unused") private int pageid;
+    @SuppressWarnings("unused") private int ns;
+    @SuppressWarnings("unused,NullableProblems") @NonNull private String title;
+
+    @SuppressWarnings("unused") @Nullable private List<LangLink> langlinks;
+    @Nullable public List<LangLink> langLinks() {
+        return langlinks;
+    }
+
+    @SuppressWarnings("unused") @Nullable private List<Revision> revisions;
+    @Nullable public List<Revision> revisions() {
+        return revisions;
+    }
+
+    public class Revision {
+        @SuppressWarnings("unused,NullableProblems") @NonNull private String 
contentformat;
+        @SuppressWarnings("unused,NullableProblems") @NonNull private String 
contentmodel;
+        @SerializedName("*") @SuppressWarnings("unused,NullableProblems") 
@NonNull private String content;
+        @NonNull public String content() {
+            return content;
+        }
+    }
+
+    public class LangLink {
+        @SuppressWarnings("unused,NullableProblems") @NonNull private String 
lang;
+        @NonNull public String lang() {
+            return lang;
+        }
+
+        @SerializedName("*") @SuppressWarnings("unused,NullableProblems") 
@NonNull private String localizedTitle;
+        @NonNull public String localizedTitle() {
+            return localizedTitle;
+        }
+    }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/317281
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I223090e67bb6206817041ce3669281dbae3cfdff
Gerrit-PatchSet: 1
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Mholloway <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to