jenkins-bot has submitted this change and it was merged.

Change subject: Implement background saved page syncing
......................................................................


Implement background saved page syncing

Kicks off a service on app startup to check with the new Reading List
pages DAO for added or deleted entries.  Handles adding or deleting
accordingly.

Bug: T126753
Change-Id: Iae8e9e19fa4d774f590a85f238b1510bb5642878
---
M app/src/main/AndroidManifest.xml
M app/src/main/java/org/wikipedia/WikipediaApp.java
M app/src/main/java/org/wikipedia/database/AppContentProvider.java
M app/src/main/java/org/wikipedia/database/DatabaseClient.java
M app/src/main/java/org/wikipedia/page/Namespace.java
M app/src/main/java/org/wikipedia/page/PageProperties.java
A app/src/main/java/org/wikipedia/savedpages/ReadingListPageObserver.java
A app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java
M app/src/main/java/org/wikipedia/server/PageService.java
M app/src/main/java/org/wikipedia/server/mwapi/MwPageService.java
M app/src/main/java/org/wikipedia/server/restbase/RbPageService.java
A app/src/main/java/org/wikipedia/util/DateUtil.java
M app/src/main/java/org/wikipedia/util/FileUtil.java
13 files changed, 343 insertions(+), 19 deletions(-)

Approvals:
  Niedzielski: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7f80339..d6c2bcc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -235,5 +235,7 @@
 
         <service 
android:name="com.mapbox.mapboxsdk.telemetry.TelemetryService" />
 
+        <service android:name=".savedpages.SavedPageSyncService" />
+
     </application>
 </manifest>
diff --git a/app/src/main/java/org/wikipedia/WikipediaApp.java 
b/app/src/main/java/org/wikipedia/WikipediaApp.java
index 59e6390..ee61928 100644
--- a/app/src/main/java/org/wikipedia/WikipediaApp.java
+++ b/app/src/main/java/org/wikipedia/WikipediaApp.java
@@ -3,6 +3,8 @@
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.Application;
+import android.database.ContentObserver;
+import android.net.Uri;
 import android.os.Build;
 import android.os.Handler;
 import android.support.annotation.IntRange;
@@ -22,6 +24,7 @@
 import org.wikipedia.crash.hockeyapp.HockeyAppCrashReporter;
 import org.wikipedia.database.Database;
 import org.wikipedia.database.DatabaseClient;
+import org.wikipedia.database.contract.ReadingListPageContract;
 import org.wikipedia.editing.EditTokenStorage;
 import org.wikipedia.editing.summaries.EditSummary;
 import org.wikipedia.events.ChangeTextSizeEvent;
@@ -39,6 +42,7 @@
 import org.wikipedia.readinglist.page.database.ReadingListPageHttpRow;
 import org.wikipedia.readinglist.page.database.disk.ReadingListPageDiskRow;
 import org.wikipedia.savedpages.SavedPage;
+import org.wikipedia.savedpages.ReadingListPageObserver;
 import org.wikipedia.search.RecentSearch;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.theme.Theme;
@@ -51,14 +55,11 @@
 import org.wikipedia.util.log.L;
 import org.wikipedia.zero.WikipediaZeroHandler;
 
-import java.text.SimpleDateFormat;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Random;
-import java.util.TimeZone;
 import java.util.UUID;
 
 import retrofit.RequestInterceptor;
@@ -76,6 +77,8 @@
 
     public static final int PREFERRED_THUMB_SIZE = 320;
 
+    public static final String FROM_READING_LIST_PAGE_OBSERVER = 
"fromReadingListPageObserver";
+
     private final RemoteConfig remoteConfig = new RemoteConfig();
     private final UserInfoStorage userInfoStorage = new UserInfoStorage();
     private final Map<Class<?>, DatabaseClient<?>> databaseClients = 
Collections.synchronizedMap(new HashMap<Class<?>, DatabaseClient<?>>());
@@ -83,6 +86,7 @@
     private AppLanguageState appLanguageState;
     private FunnelManager funnelManager;
     private SessionFunnel sessionFunnel;
+    private ContentObserver readingListPageObserver;
 
     private Database database;
     private EditTokenStorage editTokenStorage;
@@ -183,6 +187,7 @@
         AccountUtil.createAccountForLoggedInUser();
 
         UserOptionContentResolver.registerAppSyncObserver(this);
+        registerReadingListPageObserver();
     }
 
     public Bus getBus() {
@@ -311,6 +316,11 @@
     @Nullable
     public String getAppLanguageCanonicalName(String code) {
         return appLanguageState.getAppLanguageCanonicalName(code);
+    }
+
+    @NonNull
+    public ContentObserver getReadingListPageObserver() {
+        return readingListPageObserver;
     }
 
     public Database getDatabase() {
@@ -521,12 +531,6 @@
         return PrefsOnboardingStateMachine.getInstance();
     }
 
-    public SimpleDateFormat getSimpleDateFormat() {
-        SimpleDateFormat simpleDateFormat = new 
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
-        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-        return simpleDateFormat;
-    }
-
     /** For Retrofit requests. Keep in sync with #buildCustomHeaders */
     public void injectCustomHeaders(RequestInterceptor.RequestFacade request, 
Site site) {
         Map<String, String> headers = 
buildCustomHeaders(getAcceptLanguage(site));
@@ -583,4 +587,14 @@
         }
         return result;
     }
+
+    private void registerReadingListPageObserver() {
+        readingListPageObserver = new ReadingListPageObserver(null);
+        Uri readingListPageBaseUri = ReadingListPageContract.Disk.URI;
+        Uri uriWithQuery = readingListPageBaseUri.buildUpon()
+                .appendQueryParameter(FROM_READING_LIST_PAGE_OBSERVER, 
"false").build();
+        WikipediaApp.getInstance().getContentResolver()
+                .registerContentObserver(uriWithQuery, true, 
readingListPageObserver);
+        L.i("Registered reading list page observer");
+    }
 }
diff --git a/app/src/main/java/org/wikipedia/database/AppContentProvider.java 
b/app/src/main/java/org/wikipedia/database/AppContentProvider.java
index 240441d..85db4be 100644
--- a/app/src/main/java/org/wikipedia/database/AppContentProvider.java
+++ b/app/src/main/java/org/wikipedia/database/AppContentProvider.java
@@ -11,6 +11,7 @@
 import android.support.annotation.Nullable;
 
 import org.wikipedia.WikipediaApp;
+import org.wikipedia.database.contract.ReadingListPageContract;
 import org.wikipedia.util.log.L;
 
 import java.util.Arrays;
@@ -83,6 +84,11 @@
         SQLiteDatabase db = writableDatabase();
         int rows = db.delete(endpoint.tables(), selection, selectionArgs);
 
+        if (uri.equals(ReadingListPageContract.Page.URI)) {
+            uri = uri.buildUpon()
+                    
.appendQueryParameter(WikipediaApp.FROM_READING_LIST_PAGE_OBSERVER, "true")
+                    .build();
+        }
         notifyChange(uri);
         return rows;
     }
@@ -99,9 +105,11 @@
     }
 
     private void notifyChange(@NonNull Uri uri) {
-        if (getContentResolver() != null) {
-            getContentResolver().notifyChange(uri, null);
+        boolean notify = 
uri.getBooleanQueryParameter(WikipediaApp.FROM_READING_LIST_PAGE_OBSERVER, 
true);
+        if (getContentResolver() == null || !notify) {
+            return;
         }
+        getContentResolver().notifyChange(uri, null);
     }
 
     @Nullable private ContentResolver getContentResolver() {
diff --git a/app/src/main/java/org/wikipedia/database/DatabaseClient.java 
b/app/src/main/java/org/wikipedia/database/DatabaseClient.java
index a4e7b2b..ea9c348 100644
--- a/app/src/main/java/org/wikipedia/database/DatabaseClient.java
+++ b/app/src/main/java/org/wikipedia/database/DatabaseClient.java
@@ -9,6 +9,9 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
+import org.wikipedia.WikipediaApp;
+import org.wikipedia.readinglist.page.ReadingListPage;
+
 public class DatabaseClient<T> {
     @NonNull private final ContentProviderClient client;
     @NonNull private final DatabaseTable<T> databaseTable;
@@ -26,7 +29,13 @@
 
     public void persist(T obj) {
         try {
-            client.insert(uri(), toContentValues(obj));
+            Uri uri = uri();
+            if 
(ReadingListPage.DATABASE_TABLE.getBaseContentURI().equals(uri)) {
+                uri = uri.buildUpon()
+                        
.appendQueryParameter(WikipediaApp.FROM_READING_LIST_PAGE_OBSERVER, "true")
+                        .build();
+            }
+            client.insert(uri, toContentValues(obj));
         } catch (RemoteException e) {
             throw new RuntimeException(e);
         }
diff --git a/app/src/main/java/org/wikipedia/page/Namespace.java 
b/app/src/main/java/org/wikipedia/page/Namespace.java
index 7fe41ac..900f337 100644
--- a/app/src/main/java/org/wikipedia/page/Namespace.java
+++ b/app/src/main/java/org/wikipedia/page/Namespace.java
@@ -1,10 +1,12 @@
 package org.wikipedia.page;
 
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 
 import org.wikipedia.model.CodeEnum;
 import org.wikipedia.model.EnumCode;
 import org.wikipedia.model.EnumCodeMap;
+import org.wikipedia.util.StringUtil;
 
 // https://en.wikipedia.org/wiki/Wikipedia:Namespace
 // https://www.mediawiki.org/wiki/Extension_default_namespaces
@@ -63,6 +65,15 @@
 
     private final int code;
 
+    @Nullable
+    public String toLegacyString() {
+        String string = this == MAIN ? null : this.name();
+        if (string != null) {
+            StringUtil.capitalizeFirstChar(string.toLowerCase());
+        }
+        return string;
+    }
+
     @NonNull
     public static Namespace of(int code) {
         return MAP.get(code);
diff --git a/app/src/main/java/org/wikipedia/page/PageProperties.java 
b/app/src/main/java/org/wikipedia/page/PageProperties.java
index c4081ac..40bfd36 100644
--- a/app/src/main/java/org/wikipedia/page/PageProperties.java
+++ b/app/src/main/java/org/wikipedia/page/PageProperties.java
@@ -9,12 +9,13 @@
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
-import org.wikipedia.WikipediaApp;
 import org.wikipedia.server.PageLeadProperties;
 import org.wikipedia.util.StringUtil;
 
 import java.text.ParseException;
 import java.util.Date;
+
+import static org.wikipedia.util.DateUtil.getIso8601DateFormat;
 
 /**
  * Immutable class that contains metadata associated with a PageTitle.
@@ -61,8 +62,7 @@
         String lastModifiedText = core.getLastModified();
         if (lastModifiedText != null) {
             try {
-                
lastModified.setTime(WikipediaApp.getInstance().getSimpleDateFormat()
-                        .parse(lastModifiedText).getTime());
+                
lastModified.setTime(getIso8601DateFormat().parse(lastModifiedText).getTime());
             } catch (ParseException e) {
                 Log.d("PageProperties", "Failed to parse date: " + 
lastModifiedText);
             }
@@ -102,8 +102,7 @@
         lastModified = new Date();
         String lastModifiedText = json.optString("lastmodified");
         try {
-            
lastModified.setTime(WikipediaApp.getInstance().getSimpleDateFormat()
-                    .parse(lastModifiedText).getTime());
+            
lastModified.setTime(getIso8601DateFormat().parse(lastModifiedText).getTime());
         } catch (ParseException e) {
             Log.d("PageProperties", "Failed to parse date: " + 
lastModifiedText);
         }
@@ -283,8 +282,7 @@
         try {
             json.put("id", pageId);
             json.put("revision", revisionId);
-            json.put("lastmodified", 
WikipediaApp.getInstance().getSimpleDateFormat()
-                    .format(getLastModified()));
+            json.put("lastmodified", 
getIso8601DateFormat().format(getLastModified()));
             json.put("displaytitle", displayTitleText);
             json.put(JSON_NAME_TITLE_PRONUNCIATION_URL, titlePronunciationUrl);
             json.put(JSON_NAME_GEO, GeoMarshaller.marshal(geo));
diff --git 
a/app/src/main/java/org/wikipedia/savedpages/ReadingListPageObserver.java 
b/app/src/main/java/org/wikipedia/savedpages/ReadingListPageObserver.java
new file mode 100644
index 0000000..c533e8c
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/savedpages/ReadingListPageObserver.java
@@ -0,0 +1,26 @@
+package org.wikipedia.savedpages;
+
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+
+import org.wikipedia.WikipediaApp;
+
+public class ReadingListPageObserver extends ContentObserver {
+    public ReadingListPageObserver(@Nullable Handler handler) {
+        super(handler);
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+        onChange(selfChange, null);
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        WikipediaApp.getInstance()
+                .startService(new Intent(WikipediaApp.getInstance(), 
SavedPageSyncService.class));
+    }
+}
diff --git 
a/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java 
b/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java
new file mode 100644
index 0000000..4ff0b96
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java
@@ -0,0 +1,176 @@
+package org.wikipedia.savedpages;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+import com.github.kevinsawicki.http.HttpRequest;
+
+import org.wikipedia.WikipediaApp;
+import org.wikipedia.page.Page;
+import org.wikipedia.page.PageTitle;
+import org.wikipedia.readinglist.page.ReadingListPageRow;
+import org.wikipedia.readinglist.page.database.ReadingListPageDao;
+import org.wikipedia.readinglist.page.database.disk.ReadingListPageDiskRow;
+import org.wikipedia.server.PageService;
+import org.wikipedia.server.PageServiceFactory;
+import org.wikipedia.util.FileUtil;
+import org.wikipedia.util.UriUtil;
+import org.wikipedia.util.log.L;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static org.wikipedia.readinglist.page.database.disk.DiskStatus.DELETED;
+import static org.wikipedia.readinglist.page.database.disk.DiskStatus.ONLINE;
+import static org.wikipedia.readinglist.page.database.disk.DiskStatus.OUTDATED;
+import static org.wikipedia.readinglist.page.database.disk.DiskStatus.SAVED;
+import static org.wikipedia.readinglist.page.database.disk.DiskStatus.UNSAVED;
+import static org.wikipedia.util.FileUtil.writeFile;
+
+public class SavedPageSyncService extends IntentService {
+    @NonNull private ReadingListPageDao dao;
+
+    public SavedPageSyncService() {
+        super("SavedPageSyncService");
+        dao = ReadingListPageDao.instance();
+    }
+
+    @Override
+    protected void onHandleIntent(@NonNull Intent intent) {
+        List<ReadingListPageDiskRow> queue = new ArrayList<>();
+        Collection<ReadingListPageDiskRow> rows = dao.startDiskTransaction();
+        L.i("Syncing saved rlp pages with saved pages service");
+
+        for (final ReadingListPageDiskRow row : rows) {
+            L.v("Found pending tx with status: " + row.status().name());
+            switch (row.status()) {
+                case UNSAVED:
+                case DELETED:
+                    String filename = row.filename();
+                    if (filename != null) {
+                        FileUtil.delete(new File(filename), true);
+                        dao.completeDiskTransaction(row);
+                        L.v("Deleted" + filename);
+                        continue;
+                    }
+                    L.e("Found row with null filename; skipping");
+                    continue;
+                case OUTDATED:
+                    queue.add(row);
+                    continue;
+                case ONLINE:
+                case SAVED:
+                    L.w("Received row with unexpected status " + row.status() 
+ ": "
+                            + row.toString());
+                    continue;
+                default:
+                    throw new UnsupportedOperationException("Invalid disk row 
status: "
+                            + row.status().name());
+            }
+        }
+        saveNewEntries(queue);
+    }
+
+    @VisibleForTesting
+    public void saveNewEntries(List<ReadingListPageDiskRow> queue) {
+        while (!queue.isEmpty()) {
+            ReadingListPageDiskRow row = queue.get(0);
+            boolean ok = savePageFor(row);
+            if (!ok) {
+                dao.failDiskTransaction(queue);
+                break;
+            }
+            dao.completeDiskTransaction(row);
+            queue.remove(row);
+        }
+    }
+
+    @VisibleForTesting
+    public boolean savePageFor(@NonNull ReadingListPageDiskRow row) {
+        final PageTitle title = makeTitleFrom(row);
+        if (title == null) {
+            return false;
+        }
+
+        try {
+            final Page page = 
getApiService(title).pageCombo(title.getPrefixedText(),
+                            
!WikipediaApp.getInstance().isImageDownloadEnabled()).toPage(title);
+            final SavedPage savedPage = new SavedPage(page.getTitle());
+            final ImageUrlMap imageUrlMap = new 
ImageUrlMap.Builder(FileUtil.getSavedPageDirFor(title))
+                .extractUrls(page).build();
+            savedPage.writeToFileSystem(page);
+            downloadImages(imageUrlMap);
+            savedPage.writeUrlMap(imageUrlMap.toJSON());
+            L.i("Page " + title.getDisplayText() + " saved!");
+            return true;
+        } catch (Exception e) {
+            L.e("Failed to save page " + title.getDisplayText(), e);
+            return false;
+        }
+    }
+
+    @Nullable
+    private PageTitle makeTitleFrom(@NonNull ReadingListPageDiskRow row) {
+        ReadingListPageRow pageRow = row.dat();
+        if (pageRow == null) {
+            return null;
+        }
+        String namespace = pageRow.namespace().toLegacyString();
+        return new PageTitle(namespace, pageRow.title(), pageRow.site());
+    }
+
+    /**
+     * @param imageUrlMap a Map with entries {source URL, file path} of images 
to be downloaded
+     */
+    private void downloadImages(@NonNull final ImageUrlMap imageUrlMap) {
+        for (Map.Entry<String, String> entry : imageUrlMap.entrySet()) {
+            final String url = 
UriUtil.resolveProtocolRelativeUrl(entry.getKey());
+            final File file = new File(entry.getValue());
+            boolean success = false;
+            try {
+                success = downloadImage(url, file);
+            } catch (IOException e) {
+                L.e("Failed to download image: " + url, e);
+            }
+
+            if (!success) {
+                imageUrlMap.remove(url);
+            }
+        }
+    }
+
+    private boolean downloadImage(@NonNull  String url, @NonNull File file) 
throws IOException {
+        if (!url.startsWith("http")) {
+            L.e("ignoring non-HTTP URL " + url);
+            return true;
+        }
+
+        HttpRequest request = 
HttpRequest.get(url).userAgent(WikipediaApp.getInstance()
+                .getUserAgent());
+        try {
+            if (request.ok()) {
+                InputStream response = request.stream();
+                writeFile(response, file);
+                response.close();
+                L.v("downloaded image " + url + " to " + 
file.getAbsolutePath());
+                return true;
+            }
+        } catch (Exception e) {
+            L.e("could not download image " + url, e);
+        }
+        return false;
+    }
+
+    @NonNull
+    private PageService getApiService(@NonNull PageTitle title) {
+        return PageServiceFactory.create(title.getSite());
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/server/PageService.java 
b/app/src/main/java/org/wikipedia/server/PageService.java
index ff5826e..4514bc3 100644
--- a/app/src/main/java/org/wikipedia/server/PageService.java
+++ b/app/src/main/java/org/wikipedia/server/PageService.java
@@ -41,4 +41,12 @@
      * @param cb a Retrofit callback which provides the populated PageCombo 
object in #success
      */
     void pageCombo(String title, boolean noImages, PageCombo.Callback cb);
+
+    /**
+     * Gets all page content of a given title.  Used in the saved page sync 
background service.
+     *
+     * @param title the page title to be used including prefix
+     * @param noImages add the noimages flag to the request if true
+     */
+    PageCombo pageCombo(String title, boolean noImages);
 }
diff --git a/app/src/main/java/org/wikipedia/server/mwapi/MwPageService.java 
b/app/src/main/java/org/wikipedia/server/mwapi/MwPageService.java
index 77d49e8..78e4d66 100644
--- a/app/src/main/java/org/wikipedia/server/mwapi/MwPageService.java
+++ b/app/src/main/java/org/wikipedia/server/mwapi/MwPageService.java
@@ -93,6 +93,11 @@
         });
     }
 
+    @Override
+    public MwPageCombo pageCombo(String title, boolean noImages) {
+        return webService.pageCombo(title, noImages);
+    }
+
     /**
      * Optional boolean Retrofit parameter.
      * We don't want to send the query parameter at all when it's false since 
the presence of the
@@ -179,5 +184,19 @@
                 + "&noheadings=true")
         void pageCombo(@Query("page") String title, @Query("noimages") Boolean 
noImages,
                        Callback<MwPageCombo> cb);
+
+        /**
+         * Gets all page content of a given title -- for refreshing a saved 
page
+         * Note: the only difference in the URL from #pageLead is the 
sections=all instead of 0.
+         *
+         * @param title the page title to be used including prefix
+         * @param noImages add the noimages flag to the request if true
+         */
+        @GET("/w/api.php?action=mobileview&format=json&formatversion=2&prop="
+                + 
"text%7Csections%7Clanguagecount%7Cthumb%7Cimage%7Cid%7Crevision%7Cdescription"
+                + 
"%7Clastmodified%7Cnormalizedtitle%7Cdisplaytitle%7Cprotection%7Ceditable"
+                + 
"&onlyrequestedsections=1&sections=all&sectionprop=toclevel%7Cline%7Canchor"
+                + "&noheadings=true")
+        MwPageCombo pageCombo(@Query("page") String title, @Query("noimages") 
Boolean noImages);
     }
 }
diff --git a/app/src/main/java/org/wikipedia/server/restbase/RbPageService.java 
b/app/src/main/java/org/wikipedia/server/restbase/RbPageService.java
index 1a96b33..3886fb4 100644
--- a/app/src/main/java/org/wikipedia/server/restbase/RbPageService.java
+++ b/app/src/main/java/org/wikipedia/server/restbase/RbPageService.java
@@ -100,6 +100,11 @@
         });
     }
 
+    @Override
+    public RbPageCombo pageCombo(String title, boolean noImages) {
+        return webService.pageCombo(title, noImages);
+    }
+
     /* Not defined in the PageService interface since the Wiktionary 
definition endpoint exists only
      * in the mobile content service, and does not concern the wholesale 
retrieval of the contents
      * of a wiki page.
@@ -181,6 +186,14 @@
         void pageCombo(@Path("title") String title, @Query("noimages") Boolean 
noImages,
                        Callback<RbPageCombo> cb);
 
+        /**
+         * Gets all page content of a given title.  Used in the saved page 
sync background service.
+         *
+         * @param title the page title to be used including prefix
+         * @param noImages add the noimages flag to the request if true
+         */
+        @GET("/page/mobile-sections/{title}")
+        RbPageCombo pageCombo(@Path("title") String title, @Query("noimages") 
Boolean noImages);
 
         /**
          * Gets selected Wiktionary content for a given title derived from 
user-selected text
diff --git a/app/src/main/java/org/wikipedia/util/DateUtil.java 
b/app/src/main/java/org/wikipedia/util/DateUtil.java
new file mode 100644
index 0000000..6184a82
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/util/DateUtil.java
@@ -0,0 +1,31 @@
+package org.wikipedia.util;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public final class DateUtil {
+
+    public static SimpleDateFormat getIso8601DateFormat() {
+        SimpleDateFormat simpleDateFormat = new 
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
+        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+        return simpleDateFormat;
+    }
+
+    // Ex. "2015-07-18T18:11:52Z"
+    public static long fromMwApiTimestamp(String timestamp) {
+        long timeInMilliseconds = 0;
+        try {
+            Date date = getIso8601DateFormat().parse(timestamp);
+            timeInMilliseconds = date.getTime();
+        } catch (ParseException e) {
+            e.printStackTrace();
+        }
+        return timeInMilliseconds;
+    }
+
+    private DateUtil() {
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/util/FileUtil.java 
b/app/src/main/java/org/wikipedia/util/FileUtil.java
index 813a53e..f6b5fa3 100644
--- a/app/src/main/java/org/wikipedia/util/FileUtil.java
+++ b/app/src/main/java/org/wikipedia/util/FileUtil.java
@@ -69,6 +69,15 @@
         path.delete();
     }
 
+    public static void writeFile(InputStream inputStream, File file) throws 
IOException {
+        FileOutputStream outputStream = new FileOutputStream(file);
+        try {
+            copyStreams(inputStream, outputStream);
+        } finally {
+            outputStream.close();
+        }
+    }
+
     /**
      * Utility method to copy a stream into another stream.
      *

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Iae8e9e19fa4d774f590a85f238b1510bb5642878
Gerrit-PatchSet: 45
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Mholloway <[email protected]>
Gerrit-Reviewer: BearND <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>
Gerrit-Reviewer: Dbrant <[email protected]>
Gerrit-Reviewer: Mholloway <[email protected]>
Gerrit-Reviewer: Niedzielski <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to