Bug 1380804: Access Pocket API through loader. r?mcomella draft api
authorChenxia Liu <liuche@mozilla.com>
Wed, 02 Aug 2017 16:40:41 -0700
changeset 620860 c79cac74d72b8e020ccc8ae7353a39200b19ff9a
parent 620091 6f1914a4f241b8ac62953de069296397b7645cd1
child 640829 2d738b88a484aebc81e69e1579b61f40441c9ecd
push id72176
push usercliu@mozilla.com
push dateFri, 04 Aug 2017 01:32:06 +0000
reviewersmcomella
bugs1380804
milestone57.0a1
Bug 1380804: Access Pocket API through loader. r?mcomella MozReview-Commit-ID: AzYi5oEJ1p
mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/topstories/PocketStoriesLoader.java
mobile/android/base/moz.build
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/topstories/PocketStoriesLoader.java
@@ -0,0 +1,125 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.activitystream.homepanel.topstories;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.util.Log;
+
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.util.FileUtils;
+import org.mozilla.gecko.util.ProxySelector;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Loader implementation for loading fresh or cached content from the Pocket Stories API.
+ * {@link #loadInBackground()} returns a JSON string of Pocket stories.
+ *
+ * NB: Using AsyncTaskLoader rather than AsyncTask so that loader is tied Activity lifecycle.
+ */
+
+public class PocketStoriesLoader extends AsyncTaskLoader<String> {
+    public static String LOGTAG = "PocketStoriesLoader";
+
+    // Pocket SharedPreferences keys
+    private static final String POCKET_PREFS_FILE = "PocketStories";
+    private static final String CACHE_TIMESTAMP_MILLIS_PREFIX = "timestampMillis-";
+    private static final String STORIES_CACHE_PREFIX = "storiesCache-";
+
+    // Pocket API params and defaults
+    private static final String GLOBAL_ENDPOINT = "https://getpocket.com/v3/firefox/global-recs";
+    private static final String PARAM_APIKEY = "consumer_key";
+    private static final String APIKEY = "KEY_PLACEHOLDER"; // Bug 1386906: Add Pocket keys to builders.
+    private static final String PARAM_COUNT = "count";
+    private static final int DEFAULT_COUNT = 20;
+    private static final String PARAM_LOCALE = "locale_lang";
+
+    private static final long REFRESH_INTERVAL_MILLIS = TimeUnit.HOURS.toMillis(3);
+
+    private static final int BUFFER_SIZE = 2048;
+    private static final int CONNECT_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(15);
+    private static final int READ_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(15);
+
+    private String localeLang;
+    private final SharedPreferences sharedPreferences;
+
+    public PocketStoriesLoader(Context context) {
+        super(context);
+
+        sharedPreferences = context.getSharedPreferences(POCKET_PREFS_FILE, Context.MODE_PRIVATE);
+        localeLang = Locales.getLanguageTag(Locale.getDefault());
+    }
+
+    @Override
+    protected void onStartLoading() {
+        // Check timestamp to determine if we have cached stories. This won't properly handle a client manually
+        // changing clock times, but this is not a time-sensitive task.
+        final long previousTime = sharedPreferences.getLong(CACHE_TIMESTAMP_MILLIS_PREFIX + localeLang, 0);
+        if (System.currentTimeMillis() - previousTime > REFRESH_INTERVAL_MILLIS) {
+            forceLoad();
+        } else {
+            deliverResult(sharedPreferences.getString(STORIES_CACHE_PREFIX + localeLang, null));
+        }
+    }
+
+    @Override
+    protected void onReset() {
+        localeLang = Locales.getLanguageTag(Locale.getDefault());
+    }
+
+    @Override
+    public String loadInBackground() {
+        return makeAPIRequestWithKey(APIKEY);
+    }
+
+    protected String makeAPIRequestWithKey(final String apiKey) {
+        HttpURLConnection connection = null;
+
+        final Uri uri = Uri.parse(GLOBAL_ENDPOINT)
+                .buildUpon()
+                .appendQueryParameter(PARAM_APIKEY, apiKey)
+                .appendQueryParameter(PARAM_COUNT, String.valueOf(DEFAULT_COUNT))
+                .appendQueryParameter(PARAM_LOCALE, localeLang)
+                .build();
+        try {
+            connection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(new URI(uri.toString()));
+
+            connection.setConnectTimeout(CONNECT_TIMEOUT);
+            connection.setReadTimeout(READ_TIMEOUT);
+
+            final InputStream stream = new BufferedInputStream(connection.getInputStream());
+            final String output = FileUtils.readStringFromInputStreamAndCloseStream(stream, BUFFER_SIZE);
+
+            // Update cache and timestamp.
+            sharedPreferences.edit().putLong(CACHE_TIMESTAMP_MILLIS_PREFIX + localeLang, System.currentTimeMillis())
+                                    .putString(STORIES_CACHE_PREFIX + localeLang, output)
+                                    .apply();
+
+            return output;
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Problem opening connection or reading input stream", e);
+            return null;
+        } catch (URISyntaxException e) {
+            Log.e(LOGTAG, "Couldn't create URI", e);
+            return null;
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -519,16 +519,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'activitystream/homepanel/stream/TopPanel.java',
     'activitystream/homepanel/stream/WelcomePanel.java',
     'activitystream/homepanel/StreamItemAnimator.java',
     'activitystream/homepanel/StreamRecyclerAdapter.java',
     'activitystream/homepanel/topsites/TopSitesCard.java',
     'activitystream/homepanel/topsites/TopSitesPage.java',
     'activitystream/homepanel/topsites/TopSitesPageAdapter.java',
     'activitystream/homepanel/topsites/TopSitesPagerAdapter.java',
+    'activitystream/homepanel/topstories/PocketStoriesLoader.java',
     'activitystream/ranking/HighlightCandidate.java',
     'activitystream/ranking/HighlightCandidateCursorIndices.java',
     'activitystream/ranking/HighlightsRanking.java',
     'activitystream/ranking/RankingUtils.java',
     'activitystream/Utils.java',
     'adjust/AdjustBrowserAppDelegate.java',
     'animation/AnimationUtils.java',
     'animation/HeightChangeAnimation.java',