Bug 1380808 - Add live Pocket content. r?mcomella
MozReview-Commit-ID: CRfYcmHuUgl
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/ActivityStreamPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/ActivityStreamPanel.java
@@ -13,47 +13,49 @@ import android.support.v4.content.Contex
import android.support.v4.content.Loader;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import org.mozilla.gecko.R;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
+import org.mozilla.gecko.activitystream.homepanel.model.TopStory;
+import org.mozilla.gecko.activitystream.homepanel.topstories.PocketStoriesLoader;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.activitystream.homepanel.model.Highlight;
import org.mozilla.gecko.activitystream.homepanel.topsites.TopSitesPagerAdapter;
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
import java.util.Collections;
import java.util.List;
public class ActivityStreamPanel extends FrameLayout {
private final StreamRecyclerAdapter adapter;
private static final int LOADER_ID_HIGHLIGHTS = 0;
private static final int LOADER_ID_TOPSITES = 1;
+ private static final int LOADER_ID_POCKET = 2;
/**
* Number of database entries to consider and rank for finding highlights.
*/
private static final int HIGHLIGHTS_CANDIDATES = 500;
/**
* Number of highlights that should be returned (max).
*/
private static final int HIGHLIGHTS_LIMIT = 10;
public static final int TOP_SITES_COLUMNS = 4;
public static final int TOP_SITES_ROWS = 2;
private int desiredTileWidth;
- private int desiredTilesHeight;
private int tileMargin;
public ActivityStreamPanel(Context context, AttributeSet attrs) {
super(context, attrs);
setBackgroundColor(ContextCompat.getColor(context, R.color.about_page_header_grey));
inflate(context, R.layout.as_content, this);
@@ -70,32 +72,32 @@ public class ActivityStreamPanel extends
rv.addItemDecoration(new HighlightsDividerItemDecoration(context));
RecyclerViewClickSupport.addTo(rv)
.setOnItemClickListener(adapter)
.setOnItemLongClickListener(adapter);
final Resources resources = getResources();
desiredTileWidth = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_width);
- desiredTilesHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_height);
tileMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin);
ActivityStreamTelemetry.Extras.setGlobal(
ActivityStreamTelemetry.Contract.FX_ACCOUNT_PRESENT,
FirefoxAccounts.firefoxAccountsExist(context)
);
}
void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
adapter.setOnUrlOpenListeners(onUrlOpenListener, onUrlOpenInBackgroundListener);
}
public void load(LoaderManager lm) {
lm.initLoader(LOADER_ID_TOPSITES, null, new TopSitesCallback());
lm.initLoader(LOADER_ID_HIGHLIGHTS, null, new HighlightsCallbacks());
+ lm.initLoader(LOADER_ID_POCKET, null, new PocketStoriesCallbacks());
}
public void unload() {
adapter.swapHighlights(Collections.<Highlight>emptyList());
adapter.swapTopSitesCursor(null);
}
@@ -169,9 +171,29 @@ public class ActivityStreamPanel extends
adapter.swapTopSitesCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapTopSitesCursor(null);
}
}
+
+ private class PocketStoriesCallbacks implements LoaderManager.LoaderCallbacks<List<TopStory>> {
+
+ @Override
+ public Loader<List<TopStory>> onCreateLoader(int id, Bundle args) {
+ return new PocketStoriesLoader(getContext());
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<TopStory>> loader, List<TopStory> data) {
+ adapter.swapTopStories(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<TopStory>> loader) {
+ adapter.swapTopStories(Collections.<TopStory>emptyList());
+ }
+
+
+ }
}
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/StreamRecyclerAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/StreamRecyclerAdapter.java
@@ -89,17 +89,16 @@ public class StreamRecyclerAdapter exten
public StreamRecyclerAdapter() {
setHasStableIds(true);
recyclerViewModel = new LinkedList<>();
for (RowItemType type : FIXED_ROWS) {
recyclerViewModel.add(makeRowModelFromType(type));
}
topStoriesQueue = Collections.emptyList();
- loadTopStories();
}
void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
this.onUrlOpenListener = onUrlOpenListener;
this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
}
public void setTileSize(int tiles, int tilesSize) {
@@ -300,24 +299,30 @@ public class StreamRecyclerAdapter exten
}
public void swapHighlights(List<Highlight> highlights) {
recyclerViewModel = recyclerViewModel.subList(0, FIXED_ROWS.length + getNumOfTypeShown(RowItemType.TOP_STORIES_ITEM));
recyclerViewModel.addAll(highlights);
notifyDataSetChanged();
}
- private void loadTopStories() {
- List<TopStory> newStories = makePlaceholderStories();
- topStoriesQueue = newStories;
+ public void swapTopStories(List<TopStory> newStories) {
+ final int insertionIndex = indexOfType(RowItemType.TOP_STORIES_TITLE, recyclerViewModel) + 1;
+ int numOldStories = getNumOfTypeShown(RowItemType.TOP_STORIES_ITEM);
+ while (numOldStories > 0) {
+ recyclerViewModel.remove(insertionIndex);
+ numOldStories--;
+ }
- final int insertionIndex = indexOfType(RowItemType.TOP_STORIES_TITLE, recyclerViewModel) + 1;
- for (int i = 0; i < Math.min(MAX_TOP_STORIES, newStories.size()); i++) {
- recyclerViewModel.add(insertionIndex + i, newStories.get(i));
+ topStoriesQueue = newStories;
+ for (int i = 0; i < Math.min(MAX_TOP_STORIES, topStoriesQueue.size()); i++) {
+ recyclerViewModel.add(insertionIndex + i, topStoriesQueue.get(i));
}
+
+ notifyDataSetChanged();
}
/**
* Returns the index of the first item of the type found.
* @param type viewType of RowItemType
* @param rowModelList List to be indexed into
* @return index of first item of the type, or -1 if it none exist.
*/
@@ -350,25 +355,16 @@ public class StreamRecyclerAdapter exten
count++;
} else {
break;
}
}
return count;
}
- private List<TopStory> makePlaceholderStories() {
- final List<TopStory> stories = new LinkedList<>();
- final String[] TITLES = { "Placeholder 1", "Placeholder 2", "Placeholder 3"};
- for (String title : TITLES) {
- stories.add(new TopStory(title, "https://www.mozilla.org/"));
- }
- return stories;
- }
-
public void swapTopSitesCursor(Cursor cursor) {
this.topSitesCursor = cursor;
notifyItemChanged(0);
}
@Override
public long getItemId(int position) {
final int viewType = getItemViewType(position);
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/topstories/PocketStoriesLoader.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/topstories/PocketStoriesLoader.java
@@ -1,32 +1,39 @@
/* -*- 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.support.v4.content.AsyncTaskLoader;
+import android.text.TextUtils;
import android.util.Log;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.activitystream.homepanel.model.TopStory;
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.LinkedList;
+import java.util.List;
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.
@@ -35,30 +42,30 @@ import java.util.concurrent.TimeUnit;
* Add the following to your mozconfig to compile with the Pocket Stories:
*
* export MOZ_ANDROID_POCKET=1
* ac_add_options --with-pocket-api-keyfile=$topsrcdir/mobile/android/base/pocket-api-sandbox.token
*
* and include the Pocket API token in the token file.
*/
-public class PocketStoriesLoader extends AsyncTaskLoader<String> {
+public class PocketStoriesLoader extends AsyncTaskLoader<List<TopStory>> {
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 = AppConstants.MOZ_POCKET_API_KEY;
private static final String PARAM_COUNT = "count";
- private static final int DEFAULT_COUNT = 20;
+ private static final int DEFAULT_COUNT = 3;
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);
@@ -69,38 +76,39 @@ public class PocketStoriesLoader extends
super(context);
sharedPreferences = context.getSharedPreferences(POCKET_PREFS_FILE, Context.MODE_PRIVATE);
localeLang = Locales.getLanguageTag(Locale.getDefault());
}
@Override
protected void onStartLoading() {
+ if (APIKEY == null) {
+ deliverResult(makePlaceholderStories());
+ return;
+ }
// 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));
+ deliverResult(jsonStringToTopStories(sharedPreferences.getString(STORIES_CACHE_PREFIX + localeLang, null)));
}
}
@Override
protected void onReset() {
localeLang = Locales.getLanguageTag(Locale.getDefault());
}
@Override
- public String loadInBackground() {
- if (APIKEY == null) {
- Log.e(LOGTAG, "Missing Pocket API key! See class comment about how to set up a mozconfig.");
- return null;
- }
- return makeAPIRequestWithKey(APIKEY);
+ public List<TopStory> loadInBackground() {
+ final String response = makeAPIRequestWithKey(APIKEY);
+ return jsonStringToTopStories(response);
}
protected String makeAPIRequestWithKey(final String apiKey) {
HttpURLConnection connection = null;
final Uri uri = Uri.parse(GLOBAL_ENDPOINT)
.buildUpon()
.appendQueryParameter(PARAM_APIKEY, apiKey)
@@ -111,28 +119,65 @@ public class PocketStoriesLoader extends
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();
+ if (!TextUtils.isEmpty(output)) {
+ // 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();
}
}
}
+ private static List<TopStory> jsonStringToTopStories(String jsonResponse) {
+ final List<TopStory> topStories = new LinkedList<>();
+
+ if (TextUtils.isEmpty(jsonResponse)) {
+ return topStories;
+ }
+
+ try {
+ final JSONObject jsonObject = new JSONObject(jsonResponse);
+ JSONArray arr = jsonObject.getJSONArray("list");
+ for (int i = 0; i < arr.length(); i++) {
+ try {
+ final JSONObject item = arr.getJSONObject(i);
+ final String title = item.getString("title");
+ final String url = item.getString("dedupe_url");
+ final String imageUrl = item.getString("image_src");
+ topStories.add(new TopStory(title, url, imageUrl));
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Couldn't parse fields in Pocket response", e);
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "Couldn't load Pocket response", e);
+ }
+ return topStories;
+ }
+
+ private static List<TopStory> makePlaceholderStories() {
+ final List<TopStory> stories = new LinkedList<>();
+ final String[] TITLES = {"Placeholder 1", "Placeholder 2", "Placeholder 3"};
+ for (String title : TITLES) {
+ stories.add(new TopStory(title, "https://www.mozilla.org/", null));
+ }
+ return stories;
+ }
}