diff options
27 files changed, 861 insertions, 163 deletions
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java index ed00ff5..9d1eb0a 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java @@ -89,6 +89,7 @@ import org.chromium.chrome.browser.metrics.StartupMetrics; import org.chromium.chrome.browser.metrics.UmaSessionStats; import org.chromium.chrome.browser.nfc.BeamController; import org.chromium.chrome.browser.nfc.BeamProvider; +import org.chromium.chrome.browser.ntp.snippets.SnippetsController; import org.chromium.chrome.browser.offlinepages.OfflinePageUtils; import org.chromium.chrome.browser.omaha.UpdateMenuItemHelper; import org.chromium.chrome.browser.pageinfo.WebsiteSettingsPopup; @@ -693,6 +694,12 @@ public abstract class ChromeActivity extends AsyncInitializationActivity mToolbarManager.onDeferredStartup(getOnCreateTimestampMs(), simpleName); } recordKeyboardLocaleUma(); + + // TODO(treib): Remove this when we have the proper morning reads fetching logic in place + if (CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_NTP_SNIPPETS)) { + // Initialize snippets + SnippetsController.get(this).fetchSnippets(false); + } } @Override diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java index b7b1237..f01a0f9 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java @@ -47,6 +47,7 @@ import org.chromium.chrome.browser.ntp.LogoBridge.LogoObserver; import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; import org.chromium.chrome.browser.ntp.interests.InterestsPage; import org.chromium.chrome.browser.ntp.interests.InterestsPage.InterestsClickListener; +import org.chromium.chrome.browser.ntp.snippets.SnippetsManager; import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; import org.chromium.chrome.browser.preferences.DocumentModeManager; import org.chromium.chrome.browser.preferences.DocumentModePreference; @@ -120,6 +121,7 @@ public class NewTabPage private String mAnimatedLogoUrl; private FakeboxDelegate mFakeboxDelegate; private OfflinePageBridge mOfflinePageBridge; + private SnippetsManager mSnippetsManager; // The timestamp at which the constructor was called. private final long mConstructedTimeNs; @@ -587,10 +589,12 @@ public class NewTabPage mLogoBridge = new LogoBridge(mProfile); updateSearchProviderHasLogo(); + mSnippetsManager = new SnippetsManager(mNewTabPageManager, mProfile); + LayoutInflater inflater = LayoutInflater.from(activity); mNewTabPageView = (NewTabPageView) inflater.inflate(R.layout.new_tab_page, null); mNewTabPageView.initialize(mNewTabPageManager, isInSingleUrlBarMode(activity), - mSearchProviderHasLogo); + mSearchProviderHasLogo, mSnippetsManager); mIsTablet = DeviceFormFactor.isTablet(activity); @@ -772,6 +776,10 @@ public class NewTabPage mLogoBridge.destroy(); mLogoBridge = null; } + if (mSnippetsManager != null) { + mSnippetsManager.destroy(); + mSnippetsManager = null; + } if (mMostVisitedItemRemovedController != null) { mTab.getSnackbarManager().dismissSnackbars(mMostVisitedItemRemovedController); } diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java index 797d04d..43c35c7 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java @@ -87,7 +87,6 @@ public class NewTabPageView extends FrameLayout private NewTabPageManager mManager; private MostVisitedDesign mMostVisitedDesign; private MostVisitedItem[] mMostVisitedItems; - private SnippetsManager mSnippetsManager; private boolean mFirstShow = true; private boolean mSearchProviderHasLogo = true; private boolean mHasReceivedMostVisitedSites; @@ -250,7 +249,7 @@ public class NewTabPageView extends FrameLayout * @param searchProviderHasLogo Whether the search provider has a logo. */ public void initialize(NewTabPageManager manager, boolean isSingleUrlBarMode, - boolean searchProviderHasLogo) { + boolean searchProviderHasLogo, SnippetsManager snippetsManager) { mManager = manager; mScrollView = (NewTabScrollView) findViewById(R.id.ntp_scrollview); @@ -340,7 +339,6 @@ public class NewTabPageView extends FrameLayout RecordHistogram.recordEnumeratedHistogram(SnippetsManager.SNIPPETS_STATE_HISTOGRAM, SnippetsManager.SNIPPETS_SHOWN, SnippetsManager.NUM_SNIPPETS_ACTIONS); mSnippetsView.setLayoutManager(new LinearLayoutManager(getContext())); - mSnippetsManager = new SnippetsManager(mManager, mSnippetsView); mSnippetsView.addOnScrollListener(new RecyclerView.OnScrollListener() { private boolean mScrolledOnce = false; @Override @@ -355,6 +353,7 @@ public class NewTabPageView extends FrameLayout SnippetsManager.NUM_SNIPPETS_ACTIONS); } }); + snippetsManager.setSnippetsView(mSnippetsView); } // Set up interests diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetCardItemViewHolder.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetCardItemViewHolder.java index 14adb83..d032d52 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetCardItemViewHolder.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetCardItemViewHolder.java @@ -102,7 +102,7 @@ class SnippetCardItemViewHolder extends SnippetListItemViewHolder implements Vie mHeadlineTextView.setText(item.mTitle); mPublisherTextView.setText(item.mPublisher); - mArticleSnippetTextView.setText(item.mSnippet); + mArticleSnippetTextView.setText(item.mPreviewText); mUrl = item.mUrl; mPosition = item.mPosition; diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsBridge.java new file mode 100644 index 0000000..db0b5ad --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsBridge.java @@ -0,0 +1,54 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.ntp.snippets; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.chrome.browser.profiles.Profile; + +/** + * Provides access to the snippets to display on the NTP using the C++ NTP Snippets Service + */ +public class SnippetsBridge { + private static final String TAG = "SnippetsBridge"; + + private long mNativeSnippetsBridge; + + /** + * An observer for notifying when new snippets are loaded + */ + public interface SnippetsObserver { + @CalledByNative("SnippetsObserver") + public void onSnippetsAvailable( + String[] titles, String[] urls, String[] thumbnailUrls, String[] previewText); + } + + /** + * Creates a SnippetsBridge for getting snippet data for the current user + * + * @param profile Profile of the user that we will retrieve snippets for. + */ + public SnippetsBridge(Profile profile, final SnippetsObserver observer) { + SnippetsObserver wrappedObserver = new SnippetsObserver() { + @Override + public void onSnippetsAvailable( + String[] titles, String[] urls, String[] thumbnailUrls, String[] previewText) { + // Don't notify observer if we've already been destroyed. + if (mNativeSnippetsBridge != 0) { + observer.onSnippetsAvailable(titles, urls, thumbnailUrls, previewText); + } + } + }; + mNativeSnippetsBridge = nativeInit(profile, wrappedObserver); + } + + void destroy() { + assert mNativeSnippetsBridge != 0; + nativeDestroy(mNativeSnippetsBridge); + mNativeSnippetsBridge = 0; + } + + private native long nativeInit(Profile profile, SnippetsObserver observer); + private native void nativeDestroy(long nativeNTPSnippetsBridge); +} diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsController.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsController.java new file mode 100644 index 0000000..55dd84c --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsController.java @@ -0,0 +1,59 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.ntp.snippets; + +import android.content.Context; + +import org.chromium.base.ThreadUtils; +import org.chromium.chrome.browser.profiles.Profile; +import org.chromium.chrome.browser.signin.SigninManager; +import org.chromium.chrome.browser.signin.SigninManager.SignInStateObserver; + +/** + * The main controller for calling into the native snippets component to fetch snippets + */ +public class SnippetsController implements SignInStateObserver { + private static SnippetsController sInstance; + + private long mNativeSnippetsController; + + public SnippetsController(Context applicationContext) { + SigninManager.get(applicationContext).addSignInStateObserver(this); + } + + /** + * Fetches new snippets + * + * @param overwrite true if an update to the current snippets should be forced, and false if + * snippets should be downloaded only if there are no existing ones. + */ + public void fetchSnippets(boolean overwrite) { + nativeFetchSnippets(Profile.getLastUsedProfile(), overwrite); + } + + /** + * Retrieve the singleton instance of this class. + * + * @param context the current context. + * @return the singleton instance. + */ + public static SnippetsController get(Context context) { + ThreadUtils.assertOnUiThread(); + if (sInstance == null) { + sInstance = new SnippetsController(context.getApplicationContext()); + } + return sInstance; + } + + @Override + public void onSignedIn() { + fetchSnippets(true); + } + + @Override + public void onSignedOut() {} + + private native void nativeFetchSnippets(Profile profile, boolean overwrite); +} diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsManager.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsManager.java index eb90180..7faf17a 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsManager.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsManager.java @@ -6,21 +6,19 @@ package org.chromium.chrome.browser.ntp.snippets; import android.graphics.drawable.Drawable; import android.os.AsyncTask; -import android.os.Environment; import android.support.v7.widget.RecyclerView; -import android.util.JsonReader; import android.widget.ImageView; import org.chromium.base.Log; import org.chromium.base.StreamUtil; -import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; +import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge.SnippetsObserver; +import org.chromium.chrome.browser.profiles.Profile; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -51,6 +49,7 @@ public class SnippetsManager { private NewTabPageManager mNewTabPageManager; private SnippetsAdapter mDataAdapter; + private SnippetsBridge mSnippetsBridge; /** Base type for anything to add to the snippets view */ @@ -86,15 +85,15 @@ public class SnippetsManager { public static class SnippetArticle implements SnippetListItem { public final String mTitle; public final String mPublisher; - public final String mSnippet; + public final String mPreviewText; public final String mUrl; public final String mThumbnailPath; public final int mPosition; private ThumbnailRenderingTask mThumbnailRenderingTask; - // Async task to create the thumbnail from a local file - // TODO(maybelle): This task to retrieve the thumbnail from local disk is temporary while + // Async task to create the thumbnail from a URL + // TODO(maybelle): This task to retrieve the thumbnail from the web is temporary while // we are prototyping this feature for clank. For the real production feature, we // will likely have to download/decode the thumbnail on the native side. private static class ThumbnailRenderingTask extends AsyncTask<String, Void, Drawable> { @@ -106,8 +105,18 @@ public class SnippetsManager { @Override protected Drawable doInBackground(String... params) { - String thumbnailPath = params[0]; - return Drawable.createFromPath(thumbnailPath); + InputStream is = null; + try { + is = (InputStream) new URL(params[0]).getContent(); + return Drawable.createFromStream(is, "thumbnail"); + } catch (MalformedURLException e) { + Log.e(TAG, "Could not get image thumbnail due to malformed URL", e); + } catch (IOException e) { + Log.e(TAG, "Could not get image thumbnail", e); + } finally { + StreamUtil.closeQuietly(is); + } + return null; } @Override @@ -116,11 +125,11 @@ public class SnippetsManager { } } - public SnippetArticle(String title, String publisher, String snippet, String url, + public SnippetArticle(String title, String publisher, String previewText, String url, String thumbnailPath, int position) { mTitle = title; mPublisher = publisher; - mSnippet = snippet; + mPreviewText = previewText; mUrl = url; mThumbnailPath = thumbnailPath; mPosition = position; @@ -146,117 +155,33 @@ public class SnippetsManager { } } - private class ReadFileTask extends AsyncTask<Void, Void, List<SnippetListItem>> { - private int mNumArticles; - - @Override - protected List<SnippetListItem> doInBackground(Void... params) { - FileInputStream fis = null; - try { - fis = new FileInputStream( - new File(Environment.getExternalStorageDirectory().getPath() - + "/chrome/reading_list.json")); - List<SnippetListItem> listSnippetsGroups = readJsonStream(fis); - return listSnippetsGroups; - } catch (IOException ex) { - Log.e(TAG, "Exception reading file: %s ", ex); - } finally { - StreamUtil.closeQuietly(fis); - } - return null; - } - - @Override - protected void onPostExecute(List<SnippetListItem> listSnippetsGroups) { - if (listSnippetsGroups == null) return; - - mDataAdapter.setSnippetListItems(listSnippetsGroups); - } - - private List<SnippetListItem> readJsonStream(InputStream in) throws IOException { - JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8")); - try { - return readRecommendationsArray(reader); - } finally { - reader.close(); - } - } - - private List<SnippetListItem> readRecommendationsArray(JsonReader reader) - throws IOException { - List<SnippetListItem> listSnippetItems = new ArrayList<SnippetListItem>(); - mNumArticles = 0; - reader.beginArray(); - while (reader.hasNext()) { - readSnippetGroup(listSnippetItems, reader); - } - reader.endArray(); - RecordHistogram.recordSparseSlowlyHistogram( - "NewTabPage.Snippets.NumArticles", mNumArticles); - return listSnippetItems; - } - - private void readSnippetGroup(List<SnippetListItem> listSnippetItems, JsonReader reader) - throws IOException { - reader.beginObject(); - while (reader.hasNext()) { - String name = reader.nextName(); - if (name.equals("recommendation_basis")) { - listSnippetItems.add(new SnippetHeader(reader.nextString())); - } else if (name.equals("articles")) { - readArticlesArray(listSnippetItems, reader); - } - } - reader.endObject(); - } - - private void readArticlesArray(List<SnippetListItem> listSnippetItems, JsonReader reader) - throws IOException { - reader.beginArray(); - while (reader.hasNext()) { - listSnippetItems.add(readArticleDetails(reader)); - } - reader.endArray(); - } - - private SnippetArticle readArticleDetails(JsonReader reader) throws IOException { - String headline = ""; - String publisher = ""; - String snippets = ""; - String url = ""; - String thumbnail = ""; - - reader.beginObject(); - while (reader.hasNext()) { - String name = reader.nextName(); - if (name.equals("headline")) { - headline = reader.nextString(); - } else if (name.equals("publisher")) { - publisher = reader.nextString(); - } else if (name.equals("snippet")) { - snippets = reader.nextString(); - } else if (name.equals("url")) { - url = reader.nextString(); - } else if (name.equals("thumbnail")) { - thumbnail = reader.nextString(); - } else { - reader.skipValue(); + public SnippetsManager(NewTabPageManager tabManager, Profile profile) { + mNewTabPageManager = tabManager; + mDataAdapter = new SnippetsAdapter(this); + mSnippetsBridge = new SnippetsBridge(profile, new SnippetsObserver() { + @Override + public void onSnippetsAvailable( + String[] titles, String[] urls, String[] thumbnailUrls, String[] previewText) { + List<SnippetListItem> listItems = new ArrayList<SnippetListItem>(); + for (int i = 0; i < titles.length; ++i) { + SnippetArticle article = new SnippetArticle( + titles[i], "", previewText[i], urls[i], thumbnailUrls[i], i); + listItems.add(article); } + mDataAdapter.setSnippetListItems(listItems); } - reader.endObject(); - return new SnippetsManager.SnippetArticle( - headline, publisher, snippets, url, thumbnail, mNumArticles++); - } + }); } - public SnippetsManager(NewTabPageManager tabManager, RecyclerView mSnippetsView) { - mNewTabPageManager = tabManager; - mDataAdapter = new SnippetsAdapter(this); + public void setSnippetsView(RecyclerView mSnippetsView) { mSnippetsView.setAdapter(mDataAdapter); - new ReadFileTask().execute(); } public void loadUrl(String url) { mNewTabPageManager.open(url); } + + public void destroy() { + mSnippetsBridge.destroy(); + } } diff --git a/chrome/browser/android/DEPS b/chrome/browser/android/DEPS index 5894717..4fc151f 100644 --- a/chrome/browser/android/DEPS +++ b/chrome/browser/android/DEPS @@ -2,6 +2,7 @@ include_rules = [ "-components/devtools_bridge", "+components/devtools_discovery", "+components/devtools_http_handler", + "+components/ntp_snippets", "+components/service_tab_launcher", "+components/toolbar", "+components/web_contents_delegate_android", diff --git a/chrome/browser/android/chrome_jni_registrar.cc b/chrome/browser/android/chrome_jni_registrar.cc index d52c26b..53305a1 100644 --- a/chrome/browser/android/chrome_jni_registrar.cc +++ b/chrome/browser/android/chrome_jni_registrar.cc @@ -60,6 +60,8 @@ #include "chrome/browser/android/most_visited_sites.h" #include "chrome/browser/android/net/external_estimate_provider_android.h" #include "chrome/browser/android/new_tab_page_prefs.h" +#include "chrome/browser/android/ntp_snippets_bridge.h" +#include "chrome/browser/android/ntp_snippets_controller.h" #include "chrome/browser/android/offline_pages/offline_page_bridge.h" #include "chrome/browser/android/omnibox/answers_image_bridge.h" #include "chrome/browser/android/omnibox/autocomplete_controller_android.h" @@ -305,6 +307,8 @@ static base::android::RegistrationMethod kChromeRegisteredMethods[] = { {"NewTabPagePrefs", NewTabPagePrefs::RegisterNewTabPagePrefs}, {"NotificationUIManager", NotificationUIManagerAndroid::RegisterNotificationUIManager}, + {"NTPSnippetsBridge", NTPSnippetsBridge::Register}, + {"NTPSnippetsController", NTPSnippetsController::Register}, {"OAuth2TokenServiceDelegateAndroid", OAuth2TokenServiceDelegateAndroid::Register}, {"OfflinePageBridge", offline_pages::android::RegisterOfflinePageBridge}, diff --git a/chrome/browser/android/ntp_snippets_bridge.cc b/chrome/browser/android/ntp_snippets_bridge.cc new file mode 100644 index 0000000..1613ae42 --- /dev/null +++ b/chrome/browser/android/ntp_snippets_bridge.cc @@ -0,0 +1,84 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/android/ntp_snippets_bridge.h" + +#include <jni.h> + +#include "base/android/jni_android.h" +#include "base/android/jni_array.h" +#include "base/android/scoped_java_ref.h" +#include "chrome/browser/ntp_snippets/ntp_snippets_service_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_android.h" +#include "components/ntp_snippets/ntp_snippet.h" +#include "components/ntp_snippets/ntp_snippets_service.h" +#include "jni/SnippetsBridge_jni.h" + +using base::android::JavaParamRef; +using base::android::ToJavaArrayOfStrings; +using ntp_snippets::NTPSnippetsService; +using ntp_snippets::NTPSnippetsServiceObserver; + +static jlong Init(JNIEnv* env, + const JavaParamRef<jobject>& obj, + const JavaParamRef<jobject>& j_profile, + const JavaParamRef<jobject>& j_observer) { + NTPSnippetsBridge* snippets_bridge = + new NTPSnippetsBridge(env, j_profile, j_observer); + return reinterpret_cast<intptr_t>(snippets_bridge); +} + +NTPSnippetsBridge::NTPSnippetsBridge(JNIEnv* env, + jobject j_profile, + jobject j_observer) + : snippet_service_observer_(this) { + observer_.Reset(env, j_observer); + + Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile); + ntp_snippets_service_ = NTPSnippetsServiceFactory::GetForProfile(profile); + snippet_service_observer_.Add(ntp_snippets_service_); + if (ntp_snippets_service_->is_loaded()) + NTPSnippetsServiceLoaded(ntp_snippets_service_); +} + +NTPSnippetsBridge::~NTPSnippetsBridge() {} + +void NTPSnippetsBridge::Destroy(JNIEnv* env, const JavaParamRef<jobject>& obj) { + delete this; +} + +void NTPSnippetsBridge::NTPSnippetsServiceLoaded(NTPSnippetsService* service) { + if (observer_.is_null()) + return; + + std::vector<std::string> titles; + std::vector<std::string> urls; + std::vector<std::string> thumbnail_urls; + std::vector<std::string> snippets; + for (const ntp_snippets::NTPSnippet& snippet : *service) { + titles.push_back(snippet.title()); + urls.push_back(snippet.url().spec()); + thumbnail_urls.push_back(snippet.salient_image_url().spec()); + snippets.push_back(snippet.snippet()); + } + + JNIEnv* env = base::android::AttachCurrentThread(); + Java_SnippetsObserver_onSnippetsAvailable( + env, observer_.obj(), ToJavaArrayOfStrings(env, titles).obj(), + ToJavaArrayOfStrings(env, urls).obj(), + ToJavaArrayOfStrings(env, thumbnail_urls).obj(), + ToJavaArrayOfStrings(env, snippets).obj()); +} + +void NTPSnippetsBridge::NTPSnippetsServiceShutdown( + NTPSnippetsService* service) { + observer_.Reset(); + snippet_service_observer_.Remove(ntp_snippets_service_); +} + +// static +bool NTPSnippetsBridge::Register(JNIEnv* env) { + return RegisterNativesImpl(env); +} diff --git a/chrome/browser/android/ntp_snippets_bridge.h b/chrome/browser/android/ntp_snippets_bridge.h new file mode 100644 index 0000000..727cd27 --- /dev/null +++ b/chrome/browser/android/ntp_snippets_bridge.h @@ -0,0 +1,44 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_ANDROID_NTP_SNIPPETS_BRIDGE_H_ +#define CHROME_BROWSER_ANDROID_NTP_SNIPPETS_BRIDGE_H_ + +#include <jni.h> + +#include "base/android/scoped_java_ref.h" +#include "base/scoped_observer.h" +#include "components/ntp_snippets/ntp_snippets_service.h" + +// The C++ counterpart to NTPSnippetsBridge.java. Enables Java code to access +// the list of snippets to show on the NTP +class NTPSnippetsBridge : public ntp_snippets::NTPSnippetsServiceObserver { + public: + explicit NTPSnippetsBridge(JNIEnv* env, + jobject j_profile, + jobject j_observer); + void Destroy(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj); + + static bool Register(JNIEnv* env); + + private: + ~NTPSnippetsBridge() override; + + // NTPSnippetsServiceObserver overrides + void NTPSnippetsServiceLoaded( + ntp_snippets::NTPSnippetsService* service) override; + void NTPSnippetsServiceShutdown( + ntp_snippets::NTPSnippetsService* service) override; + + ntp_snippets::NTPSnippetsService* ntp_snippets_service_; + + base::android::ScopedJavaGlobalRef<jobject> observer_; + ScopedObserver<ntp_snippets::NTPSnippetsService, + ntp_snippets::NTPSnippetsServiceObserver> + snippet_service_observer_; + + DISALLOW_COPY_AND_ASSIGN(NTPSnippetsBridge); +}; + +#endif // CHROME_BROWSER_ANDROID_NTP_SNIPPETS_BRIDGE_H_ diff --git a/chrome/browser/android/ntp_snippets_controller.cc b/chrome/browser/android/ntp_snippets_controller.cc new file mode 100644 index 0000000..a252d52 --- /dev/null +++ b/chrome/browser/android/ntp_snippets_controller.cc @@ -0,0 +1,34 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/android/ntp_snippets_controller.h" + +#include <jni.h> + +#include "base/android/jni_android.h" +#include "base/android/scoped_java_ref.h" +#include "chrome/browser/ntp_snippets/ntp_snippets_service_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_android.h" +#include "components/ntp_snippets/ntp_snippets_service.h" +#include "jni/SnippetsController_jni.h" + +using base::android::JavaParamRef; + +static void FetchSnippets(JNIEnv* env, + const JavaParamRef<jobject>& obj, + const JavaParamRef<jobject>& jprofile, + jboolean overwrite) { + Profile* profile = ProfileAndroid::FromProfileAndroid(jprofile); + if (profile) { + ntp_snippets::NTPSnippetsService* ntp_snippets_service = + NTPSnippetsServiceFactory::GetForProfile(profile); + ntp_snippets_service->FetchSnippets(overwrite); + } +} + +// static +bool NTPSnippetsController::Register(JNIEnv* env) { + return RegisterNativesImpl(env); +} diff --git a/chrome/browser/android/ntp_snippets_controller.h b/chrome/browser/android/ntp_snippets_controller.h new file mode 100644 index 0000000..d5bc137 --- /dev/null +++ b/chrome/browser/android/ntp_snippets_controller.h @@ -0,0 +1,21 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_ANDROID_NTP_SNIPPETS_CONTROLLER_H_ +#define CHROME_BROWSER_ANDROID_NTP_SNIPPETS_CONTROLLER_H_ + +#include <jni.h> + +#include "base/android/scoped_java_ref.h" + +// The C++ counterpart to SnippetsController.java. Enables Java code to fetch +// snippets from the server +class NTPSnippetsController { + public: + static bool Register(JNIEnv* env); + + DISALLOW_COPY_AND_ASSIGN(NTPSnippetsController); +}; + +#endif // CHROME_BROWSER_ANDROID_NTP_SNIPPETS_CONTROLLER_H_ diff --git a/chrome/browser/ntp_snippets/ntp_snippets_service_factory.cc b/chrome/browser/ntp_snippets/ntp_snippets_service_factory.cc index 5730e2f..0cf2dba 100644 --- a/chrome/browser/ntp_snippets/ntp_snippets_service_factory.cc +++ b/chrome/browser/ntp_snippets/ntp_snippets_service_factory.cc @@ -8,9 +8,18 @@ #include "base/memory/singleton.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/profile_oauth2_token_service_factory.h" +#include "chrome/browser/signin/signin_manager_factory.h" #include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/ntp_snippets/ntp_snippets_fetcher.h" #include "components/ntp_snippets/ntp_snippets_service.h" +#include "components/signin/core/browser/profile_oauth2_token_service.h" +#include "components/signin/core/browser/signin_manager.h" #include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_thread.h" +#include "net/url_request/url_request_context_getter.h" + +using content::BrowserThread; // static NTPSnippetsServiceFactory* NTPSnippetsServiceFactory::GetInstance() { @@ -34,6 +43,23 @@ NTPSnippetsServiceFactory::~NTPSnippetsServiceFactory() {} KeyedService* NTPSnippetsServiceFactory::BuildServiceInstanceFor( content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + SigninManagerBase* signin_manager = + SigninManagerFactory::GetForProfile(profile); + OAuth2TokenService* token_service = + ProfileOAuth2TokenServiceFactory::GetForProfile(profile); + scoped_refptr<net::URLRequestContextGetter> request_context = + context->GetRequestContext(); + + scoped_refptr<base::SequencedTaskRunner> task_runner = + BrowserThread::GetBlockingPool() + ->GetSequencedTaskRunnerWithShutdownBehavior( + base::SequencedWorkerPool::GetSequenceToken(), + base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN); + return new ntp_snippets::NTPSnippetsService( - g_browser_process->GetApplicationLocale()); + task_runner, g_browser_process->GetApplicationLocale(), + make_scoped_ptr(new ntp_snippets::NTPSnippetsFetcher( + task_runner, signin_manager, token_service, request_context, + profile->GetPath()))); } diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index fd35607..fae5844 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -831,6 +831,10 @@ 'browser/android/new_tab_page_prefs.h', 'browser/android/new_tab_page_url_handler.cc', 'browser/android/new_tab_page_url_handler.h', + 'browser/android/ntp_snippets_bridge.cc', + 'browser/android/ntp_snippets_bridge.h', + 'browser/android/ntp_snippets_controller.cc', + 'browser/android/ntp_snippets_controller.h', 'browser/android/omnibox/answers_image_bridge.cc', 'browser/android/omnibox/answers_image_bridge.h', 'browser/android/omnibox/autocomplete_controller_android.cc', @@ -1877,10 +1881,12 @@ 'android/java/src/org/chromium/chrome/browser/net/spdyproxy/DataReductionProxySettings.java', 'android/java/src/org/chromium/chrome/browser/notifications/NotificationUIManager.java', 'android/java/src/org/chromium/chrome/browser/ntp/ForeignSessionHelper.java', + 'android/java/src/org/chromium/chrome/browser/ntp/interests/InterestsService.java', 'android/java/src/org/chromium/chrome/browser/ntp/LogoBridge.java', 'android/java/src/org/chromium/chrome/browser/ntp/NewTabPagePrefs.java', 'android/java/src/org/chromium/chrome/browser/ntp/RecentlyClosedBridge.java', - 'android/java/src/org/chromium/chrome/browser/ntp/interests/InterestsService.java', + 'android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsBridge.java', + 'android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsController.java', 'android/java/src/org/chromium/chrome/browser/offlinepages/OfflinePageBridge.java', 'android/java/src/org/chromium/chrome/browser/omnibox/AnswersImage.java', 'android/java/src/org/chromium/chrome/browser/omnibox/AutocompleteController.java', diff --git a/components/BUILD.gn b/components/BUILD.gn index 3e17b8d..4fc532d 100644 --- a/components/BUILD.gn +++ b/components/BUILD.gn @@ -51,6 +51,7 @@ test("components_unittests") { "//components/metrics:unit_tests", "//components/net_log:unit_tests", "//components/network_time:unit_tests", + "//components/ntp_snippets:unit_tests", "//components/open_from_clipboard:unit_tests", "//components/os_crypt:unit_tests", "//components/prefs:unit_tests", diff --git a/components/ntp_snippets.gypi b/components/ntp_snippets.gypi index e3267dc..551d3e6 100644 --- a/components/ntp_snippets.gypi +++ b/components/ntp_snippets.gypi @@ -20,6 +20,8 @@ 'ntp_snippets/inner_iterator.h', 'ntp_snippets/ntp_snippet.cc', 'ntp_snippets/ntp_snippet.h', + 'ntp_snippets/ntp_snippets_fetcher.cc', + 'ntp_snippets/ntp_snippets_fetcher.h', 'ntp_snippets/ntp_snippets_service.cc', 'ntp_snippets/ntp_snippets_service.h', ], diff --git a/components/ntp_snippets/BUILD.gn b/components/ntp_snippets/BUILD.gn index 89e0ccd..222ad18 100644 --- a/components/ntp_snippets/BUILD.gn +++ b/components/ntp_snippets/BUILD.gn @@ -8,6 +8,8 @@ source_set("ntp_snippets") { "inner_iterator.h", "ntp_snippet.cc", "ntp_snippet.h", + "ntp_snippets_fetcher.cc", + "ntp_snippets_fetcher.h", "ntp_snippets_service.cc", "ntp_snippets_service.h", ] @@ -15,6 +17,9 @@ source_set("ntp_snippets") { public_deps = [ "//base", "//components/keyed_service/core", + "//components/signin/core/browser", + "//google_apis", + "//net", "//url", ] } @@ -28,6 +33,9 @@ source_set("unit_tests") { deps = [ ":ntp_snippets", + "//base", + "//components/signin/core/browser:test_support", + "//net:test_support", "//testing/gtest", ] } diff --git a/components/ntp_snippets/DEPS b/components/ntp_snippets/DEPS index f0bf3d9..830c20f 100644 --- a/components/ntp_snippets/DEPS +++ b/components/ntp_snippets/DEPS @@ -1,3 +1,8 @@ include_rules = [ "+components/keyed_service/core", + "+components/signin", + "+net/base", + "+net/http", + "+net/url_request", + "+google_apis/gaia", ] diff --git a/components/ntp_snippets/ntp_snippet.cc b/components/ntp_snippets/ntp_snippet.cc index ef59e1b..9ee2029 100644 --- a/components/ntp_snippets/ntp_snippet.cc +++ b/components/ntp_snippets/ntp_snippet.cc @@ -14,14 +14,14 @@ NTPSnippet::NTPSnippet(const GURL& url) : url_(url) { NTPSnippet::~NTPSnippet() {} // static -std::unique_ptr<NTPSnippet> NTPSnippet::NTPSnippetFromDictionary( +scoped_ptr<NTPSnippet> NTPSnippet::NTPSnippetFromDictionary( const base::DictionaryValue& dict) { // Need at least the url. std::string url; if (!dict.GetString("url", &url)) return nullptr; - std::unique_ptr<NTPSnippet> snippet(new NTPSnippet(GURL(url))); + scoped_ptr<NTPSnippet> snippet(new NTPSnippet(GURL(url))); std::string site_title; if (dict.GetString("site_title", &site_title)) diff --git a/components/ntp_snippets/ntp_snippet.h b/components/ntp_snippets/ntp_snippet.h index 6ea126a..6dd2ad04 100644 --- a/components/ntp_snippets/ntp_snippet.h +++ b/components/ntp_snippets/ntp_snippet.h @@ -4,7 +4,6 @@ #ifndef COMPONENTS_NTP_SNIPPETS_NTP_SNIPPET_H_ #define COMPONENTS_NTP_SNIPPETS_NTP_SNIPPET_H_ -#include <memory> #include <string> #include "base/macros.h" @@ -30,7 +29,7 @@ class NTPSnippet { // dictionary doesn't contain at least a url. The keys in the dictionary are // expected to be the same as the property name, with exceptions documented in // the property comment. - static std::unique_ptr<NTPSnippet> NTPSnippetFromDictionary( + static scoped_ptr<NTPSnippet> NTPSnippetFromDictionary( const base::DictionaryValue& dict); // URL of the page described by this snippet. diff --git a/components/ntp_snippets/ntp_snippets_fetcher.cc b/components/ntp_snippets/ntp_snippets_fetcher.cc new file mode 100644 index 0000000..7c74fcf --- /dev/null +++ b/components/ntp_snippets/ntp_snippets_fetcher.cc @@ -0,0 +1,191 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/ntp_snippets/ntp_snippets_fetcher.h" + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/path_service.h" +#include "base/strings/stringprintf.h" +#include "base/task_runner_util.h" +#include "components/signin/core/browser/profile_oauth2_token_service.h" +#include "components/signin/core/browser/signin_manager.h" +#include "components/signin/core/browser/signin_tracker.h" +#include "net/base/load_flags.h" +#include "net/http/http_request_headers.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" + +using net::URLFetcher; +using net::URLRequestContextGetter; +using net::HttpRequestHeaders; +using net::URLRequestStatus; + +namespace ntp_snippets { + +const char kSnippetSuggestionsFilename[] = "ntp_snippets.json"; +const char kApiScope[] = "https://www.googleapis.com/auth/webhistory"; +const char kContentSnippetsServer[] = + "https://chromereader-pa.googleapis.com/v1/fetch"; +const char kAuthorizationRequestHeaderFormat[] = "Bearer %s"; + +const char kUnpersonalizedRequestParameters[] = + "{ \"response_detail_level\": \"FULL_DEBUG\", \"advanced_options\": { " + "\"local_scoring_params\": {\"content_params\" : { " + "\"only_return_personalized_results\": false } }, " + "\"global_scoring_params\": { \"num_to_return\": 10 } } }"; + +base::FilePath GetSnippetsSuggestionsPath(const base::FilePath& base_dir) { + return base_dir.AppendASCII(kSnippetSuggestionsFilename); +} + +NTPSnippetsFetcher::NTPSnippetsFetcher( + scoped_refptr<base::SequencedTaskRunner> file_task_runner, + SigninManagerBase* signin_manager, + OAuth2TokenService* token_service, + scoped_refptr<URLRequestContextGetter> url_request_context_getter, + const base::FilePath& base_download_path) + : OAuth2TokenService::Consumer("NTP_snippets"), + file_task_runner_(file_task_runner), + url_request_context_getter_(url_request_context_getter), + signin_manager_(signin_manager), + token_service_(token_service), + download_path_(GetSnippetsSuggestionsPath(base_download_path)), + waiting_for_refresh_token_(false), + weak_ptr_factory_(this) {} + +NTPSnippetsFetcher::~NTPSnippetsFetcher() { + if (waiting_for_refresh_token_) + token_service_->RemoveObserver(this); +} + +scoped_ptr<NTPSnippetsFetcher::SnippetsAvailableCallbackList::Subscription> +NTPSnippetsFetcher::AddCallback(const SnippetsAvailableCallback& callback) { + return callback_list_.Add(callback); +} + +void NTPSnippetsFetcher::FetchSnippets(bool overwrite) { + if (overwrite) { + StartFetch(); + } else { + base::PostTaskAndReplyWithResult( + file_task_runner_.get(), FROM_HERE, + base::Bind(&base::PathExists, download_path_), + base::Bind(&NTPSnippetsFetcher::OnFileExistsCheckDone, + weak_ptr_factory_.GetWeakPtr())); + } +} + +void NTPSnippetsFetcher::OnFileExistsCheckDone(bool exists) { + if (exists) { + NotifyObservers(); + } else { + StartFetch(); + } +} + +void NTPSnippetsFetcher::StartFetch() { + if (signin_manager_->IsAuthenticated()) { + StartTokenRequest(); + } else { + if (!waiting_for_refresh_token_) { + // Wait until we get a refresh token. + waiting_for_refresh_token_ = true; + token_service_->AddObserver(this); + } + } +} + +void NTPSnippetsFetcher::StartTokenRequest() { + OAuth2TokenService::ScopeSet scopes; + scopes.insert(kApiScope); + oauth_request_ = token_service_->StartRequest( + signin_manager_->GetAuthenticatedAccountId(), scopes, this); +} + +void NTPSnippetsFetcher::NotifyObservers() { + callback_list_.Notify(download_path_); +} + +//////////////////////////////////////////////////////////////////////////////// +// OAuth2TokenService::Consumer overrides +void NTPSnippetsFetcher::OnGetTokenSuccess( + const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) { + oauth_request_.reset(); + url_fetcher_ = + URLFetcher::Create(GURL(kContentSnippetsServer), URLFetcher::POST, this); + url_fetcher_->SetRequestContext(url_request_context_getter_.get()); + url_fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + HttpRequestHeaders headers; + headers.SetHeader("Authorization", + base::StringPrintf(kAuthorizationRequestHeaderFormat, + access_token.c_str())); + headers.SetHeader("Content-Type", "application/json; charset=UTF-8"); + url_fetcher_->SetExtraRequestHeaders(headers.ToString()); + url_fetcher_->SetUploadData("application/json", + kUnpersonalizedRequestParameters); + url_fetcher_->SaveResponseToTemporaryFile(file_task_runner_.get()); + url_fetcher_->Start(); +} + +void NTPSnippetsFetcher::OnGetTokenFailure( + const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) { + oauth_request_.reset(); + DLOG(ERROR) << "Unable to get token: " << error.ToString(); +} + +//////////////////////////////////////////////////////////////////////////////// +// OAuth2TokenService::Observer overrides +void NTPSnippetsFetcher::OnRefreshTokenAvailable( + const std::string& account_id) { + token_service_->RemoveObserver(this); + waiting_for_refresh_token_ = false; + StartTokenRequest(); +} + +//////////////////////////////////////////////////////////////////////////////// +// URLFetcherDelegate overrides +void NTPSnippetsFetcher::OnURLFetchComplete(const URLFetcher* source) { + DCHECK_EQ(url_fetcher_.get(), source); + + const URLRequestStatus& status = source->GetStatus(); + if (!status.is_success()) { + DLOG(WARNING) << "URLRequestStatus error " << status.error() + << " while trying to download " << source->GetURL().spec(); + return; + } + + int response_code = source->GetResponseCode(); + if (response_code != net::HTTP_OK) { + DLOG(WARNING) << "HTTP error " << response_code + << " while trying to download " << source->GetURL().spec(); + return; + } + + base::FilePath response_path; + source->GetResponseAsFilePath(false, &response_path); + + base::PostTaskAndReplyWithResult( + file_task_runner_.get(), FROM_HERE, + base::Bind(&base::Move, response_path, download_path_), + base::Bind(&NTPSnippetsFetcher::OnFileMoveDone, + weak_ptr_factory_.GetWeakPtr())); +} + +void NTPSnippetsFetcher::OnFileMoveDone(bool success) { + if (!success) { + DLOG(WARNING) << "Could not move file to " + << download_path_.LossyDisplayName(); + return; + } + + NotifyObservers(); +} + +} // namespace ntp_snippets diff --git a/components/ntp_snippets/ntp_snippets_fetcher.h b/components/ntp_snippets/ntp_snippets_fetcher.h new file mode 100644 index 0000000..2ae47ad --- /dev/null +++ b/components/ntp_snippets/ntp_snippets_fetcher.h @@ -0,0 +1,94 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_NTP_SNIPPETS_NTP_SNIPPETS_FETCHER_H_ +#define COMPONENTS_NTP_SNIPPETS_NTP_SNIPPETS_FETCHER_H_ + +#include "base/callback.h" +#include "base/callback_list.h" +#include "base/files/file_path.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/sequenced_task_runner.h" +#include "google_apis/gaia/oauth2_token_service.h" +#include "net/url_request/url_request_context_getter.h" + +namespace net { +class URLFetcher; +class URLFetcherDelegate; +} // namespace net + +class SigninManagerBase; + +namespace ntp_snippets { + +// Fetches snippet data for the NTP from the server +class NTPSnippetsFetcher : public OAuth2TokenService::Consumer, + public OAuth2TokenService::Observer, + public net::URLFetcherDelegate { + public: + using SnippetsAvailableCallback = base::Callback<void(const base::FilePath&)>; + using SnippetsAvailableCallbackList = + base::CallbackList<void(const base::FilePath&)>; + + NTPSnippetsFetcher(scoped_refptr<base::SequencedTaskRunner> file_task_runner, + SigninManagerBase* signin_manager, + OAuth2TokenService* oauth2_token_service, + scoped_refptr<net::URLRequestContextGetter> + url_request_context_getter, + const base::FilePath& base_download_path); + ~NTPSnippetsFetcher() override; + + // Fetches snippets from the server. |overwrite| is true if existing snippets + // should be overwritten. + void FetchSnippets(bool overwrite); + + // Adds a callback that is called when a new set of snippets are downloaded + scoped_ptr<SnippetsAvailableCallbackList::Subscription> AddCallback( + const SnippetsAvailableCallback& callback) WARN_UNUSED_RESULT; + + private: + void StartTokenRequest(); + void NotifyObservers(); + void OnDownloadSnippetsDone(bool success); + void OnFileExistsCheckDone(bool exists); + void OnFileMoveDone(bool success); + void StartFetch(); + + // OAuth2TokenService::Consumer overrides: + void OnGetTokenSuccess(const OAuth2TokenService::Request* request, + const std::string& access_token, + const base::Time& expiration_time) override; + void OnGetTokenFailure(const OAuth2TokenService::Request* request, + const GoogleServiceAuthError& error) override; + + // OAuth2TokenService::Observer overrides: + void OnRefreshTokenAvailable(const std::string& account_id) override; + + // URLFetcherDelegate implementation. + void OnURLFetchComplete(const net::URLFetcher* source) override; + + // The SequencedTaskRunner on which file system operations will be run. + scoped_refptr<base::SequencedTaskRunner> file_task_runner_; + + // Holds the URL request context. + scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; + + scoped_ptr<net::URLFetcher> url_fetcher_; + scoped_ptr<SigninManagerBase> signin_manager_; + scoped_ptr<OAuth2TokenService> token_service_; + scoped_ptr<OAuth2TokenService::Request> oauth_request_; + + base::FilePath download_path_; + bool waiting_for_refresh_token_; + + SnippetsAvailableCallbackList callback_list_; + + base::WeakPtrFactory<NTPSnippetsFetcher> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(NTPSnippetsFetcher); +}; +} // namespace ntp_snippets + +#endif // COMPONENTS_NTP_SNIPPETS_NTP_SNIPPETS_FETCHER_H_ diff --git a/components/ntp_snippets/ntp_snippets_service.cc b/components/ntp_snippets/ntp_snippets_service.cc index f3c0670..149ab4a 100644 --- a/components/ntp_snippets/ntp_snippets_service.cc +++ b/components/ntp_snippets/ntp_snippets_service.cc @@ -4,14 +4,36 @@ #include "components/ntp_snippets/ntp_snippets_service.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" #include "base/json/json_string_value_serializer.h" +#include "base/location.h" +#include "base/path_service.h" +#include "base/task_runner_util.h" #include "base/values.h" namespace ntp_snippets { +bool ReadFileToString(const base::FilePath& path, std::string* data) { + DCHECK(data); + bool success = base::ReadFileToString(path, data); + DLOG_IF(ERROR, !success) << "Error reading file " << path.LossyDisplayName(); + return success; +} + NTPSnippetsService::NTPSnippetsService( - const std::string& application_language_code) - : loaded_(false), application_language_code_(application_language_code) {} + scoped_refptr<base::SequencedTaskRunner> file_task_runner, + const std::string& application_language_code, + scoped_ptr<NTPSnippetsFetcher> snippets_fetcher) + : loaded_(false), + file_task_runner_(file_task_runner), + application_language_code_(application_language_code), + snippets_fetcher_(std::move(snippets_fetcher)), + weak_ptr_factory_(this) { + snippets_fetcher_callback_ = snippets_fetcher_->AddCallback( + base::Bind(&NTPSnippetsService::OnSnippetsDownloaded, + weak_ptr_factory_.GetWeakPtr())); +} NTPSnippetsService::~NTPSnippetsService() {} @@ -21,6 +43,10 @@ void NTPSnippetsService::Shutdown() { loaded_ = false; } +void NTPSnippetsService::FetchSnippets(bool overwrite) { + snippets_fetcher_->FetchSnippets(overwrite); +} + void NTPSnippetsService::AddObserver(NTPSnippetsServiceObserver* observer) { observers_.AddObserver(observer); if (loaded_) @@ -31,6 +57,14 @@ void NTPSnippetsService::RemoveObserver(NTPSnippetsServiceObserver* observer) { observers_.RemoveObserver(observer); } +void NTPSnippetsService::OnFileReadDone(const std::string* json, bool success) { + if (!success) + return; + + DCHECK(json); + LoadFromJSONString(*json); +} + bool NTPSnippetsService::LoadFromJSONString(const std::string& str) { JSONStringValueDeserializer deserializer(str); int error_code; @@ -57,16 +91,27 @@ bool NTPSnippetsService::LoadFromJSONString(const std::string& str) { const base::DictionaryValue* content = NULL; if (!dict->GetDictionary("contentInfo", &content)) return false; - std::unique_ptr<NTPSnippet> snippet = + scoped_ptr<NTPSnippet> snippet = NTPSnippet::NTPSnippetFromDictionary(*content); if (!snippet) return false; snippets_.push_back(std::move(snippet)); } loaded_ = true; + FOR_EACH_OBSERVER(NTPSnippetsServiceObserver, observers_, NTPSnippetsServiceLoaded(this)); return true; } +void NTPSnippetsService::OnSnippetsDownloaded( + const base::FilePath& download_path) { + std::string* downloaded_data = new std::string; + base::PostTaskAndReplyWithResult( + file_task_runner_.get(), FROM_HERE, + base::Bind(&ReadFileToString, download_path, downloaded_data), + base::Bind(&NTPSnippetsService::OnFileReadDone, + weak_ptr_factory_.GetWeakPtr(), base::Owned(downloaded_data))); +} + } // namespace ntp_snippets diff --git a/components/ntp_snippets/ntp_snippets_service.h b/components/ntp_snippets/ntp_snippets_service.h index 76cb3d3..f9d2a40 100644 --- a/components/ntp_snippets/ntp_snippets_service.h +++ b/components/ntp_snippets/ntp_snippets_service.h @@ -11,19 +11,22 @@ #include <vector> #include "base/macros.h" +#include "base/memory/weak_ptr.h" #include "base/observer_list.h" +#include "base/sequenced_task_runner.h" #include "components/keyed_service/core/keyed_service.h" #include "components/ntp_snippets/inner_iterator.h" #include "components/ntp_snippets/ntp_snippet.h" +#include "components/ntp_snippets/ntp_snippets_fetcher.h" namespace ntp_snippets { class NTPSnippetsServiceObserver; // Stores and vend fresh content data for the NTP. -class NTPSnippetsService : public KeyedService { +class NTPSnippetsService : public KeyedService, NTPSnippetsFetcher::Observer { public: - using NTPSnippetStorage = std::vector<std::unique_ptr<NTPSnippet>>; + using NTPSnippetStorage = std::vector<scoped_ptr<NTPSnippet>>; using const_iterator = InnerIterator<NTPSnippetStorage::const_iterator, const NTPSnippet>; @@ -31,9 +34,15 @@ class NTPSnippetsService : public KeyedService { // 'en' or 'en-US'. Note that this code should only specify the language, not // the locale, so 'en_US' (english language with US locale) and 'en-GB_US' // (British english person in the US) are not language code. - explicit NTPSnippetsService(const std::string& application_language_code); + NTPSnippetsService(scoped_refptr<base::SequencedTaskRunner> file_task_runner, + const std::string& application_language_code, + scoped_ptr<NTPSnippetsFetcher> snippets_fetcher); ~NTPSnippetsService() override; + // Fetches snippets from the server. |overwrite| is true if existing snippets + // should be overwritten. + void FetchSnippets(bool overwrite); + // Inherited from KeyedService. void Shutdown() override; @@ -44,11 +53,6 @@ class NTPSnippetsService : public KeyedService { void AddObserver(NTPSnippetsServiceObserver* observer); void RemoveObserver(NTPSnippetsServiceObserver* observer); - // Expects the JSON to be a list of dictionaries with keys matching the - // properties of a snippet (url, title, site_title, etc...). The url is the - // only mandatory value. - bool LoadFromJSONString(const std::string& str); - // Number of snippets available. Can only be called when is_loaded() is true. NTPSnippetStorage::size_type size() { DCHECK(loaded_); @@ -72,15 +76,44 @@ class NTPSnippetsService : public KeyedService { } private: + void OnFileReadDone(const std::string* json, bool success); + void OnSnippetsDownloaded(const base::FilePath& download_path); + + // Expects the JSON to be a list of dictionaries with keys matching the + // properties of a snippet (url, title, site_title, etc...). The url is the + // only mandatory value. + bool LoadFromJSONString(const std::string& str); + // True if the suggestions are loaded. bool loaded_; + + // The SequencedTaskRunner on which file system operations will be run. + scoped_refptr<base::SequencedTaskRunner> file_task_runner_; + // All the suggestions. NTPSnippetStorage snippets_; + // The ISO 639-1 code of the language used by the application. const std::string application_language_code_; + // The observers. base::ObserverList<NTPSnippetsServiceObserver> observers_; + // The snippets fetcher + scoped_ptr<NTPSnippetsFetcher> snippets_fetcher_; + + // The callback from the snippets fetcher + scoped_ptr<NTPSnippetsFetcher::SnippetsAvailableCallbackList::Subscription> + snippets_fetcher_callback_; + + base::WeakPtrFactory<NTPSnippetsService> weak_ptr_factory_; + + friend class NTPSnippetsServiceTest; + FRIEND_TEST_ALL_PREFIXES(NTPSnippetsServiceTest, Loop); + FRIEND_TEST_ALL_PREFIXES(NTPSnippetsServiceTest, Full); + FRIEND_TEST_ALL_PREFIXES(NTPSnippetsServiceTest, ObserverLoaded); + FRIEND_TEST_ALL_PREFIXES(NTPSnippetsServiceTest, ObserverNotLoaded); + DISALLOW_COPY_AND_ASSIGN(NTPSnippetsService); }; diff --git a/components/ntp_snippets/ntp_snippets_service_unittest.cc b/components/ntp_snippets/ntp_snippets_service_unittest.cc index df1cf7b..f801e94 100644 --- a/components/ntp_snippets/ntp_snippets_service_unittest.cc +++ b/components/ntp_snippets/ntp_snippets_service_unittest.cc @@ -3,25 +3,31 @@ // found in the LICENSE file. #include "base/macros.h" +#include "base/message_loop/message_loop.h" +#include "base/thread_task_runner_handle.h" #include "base/time/time.h" #include "components/ntp_snippets/ntp_snippet.h" +#include "components/ntp_snippets/ntp_snippets_fetcher.h" #include "components/ntp_snippets/ntp_snippets_service.h" +#include "components/signin/core/browser/account_tracker_service.h" +#include "components/signin/core/browser/fake_profile_oauth2_token_service.h" +#include "components/signin/core/browser/fake_signin_manager.h" +#include "components/signin/core/browser/test_signin_client.h" +#include "net/url_request/url_request_test_util.h" #include "testing/gtest/include/gtest/gtest.h" -namespace { +namespace ntp_snippets { -class SnippetObserver : public ntp_snippets::NTPSnippetsServiceObserver { +class SnippetObserver : public NTPSnippetsServiceObserver { public: SnippetObserver() : loaded_(false), shutdown_(false) {} ~SnippetObserver() override {} - void NTPSnippetsServiceLoaded( - ntp_snippets::NTPSnippetsService* service) override { + void NTPSnippetsServiceLoaded(NTPSnippetsService* service) override { loaded_ = true; } - void NTPSnippetsServiceShutdown( - ntp_snippets::NTPSnippetsService* service) override { + void NTPSnippetsServiceShutdown(NTPSnippetsService* service) override { shutdown_ = true; loaded_ = false; } @@ -38,22 +44,45 @@ class NTPSnippetsServiceTest : public testing::Test { NTPSnippetsServiceTest() {} ~NTPSnippetsServiceTest() override {} + void SetUp() override { + signin_client_.reset(new TestSigninClient(nullptr)); + account_tracker_.reset(new AccountTrackerService()); + } + + protected: + scoped_ptr<NTPSnippetsService> CreateSnippetService() { + scoped_refptr<base::SingleThreadTaskRunner> task_runner( + base::ThreadTaskRunnerHandle::Get()); + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter = + new net::TestURLRequestContextGetter(task_runner.get()); + FakeProfileOAuth2TokenService* token_service = + new FakeProfileOAuth2TokenService(); + FakeSigninManagerBase* signin_manager = new FakeSigninManagerBase( + signin_client_.get(), account_tracker_.get()); + + scoped_ptr<NTPSnippetsService> service( + new NTPSnippetsService(task_runner.get(), std::string("fr"), + make_scoped_ptr(new NTPSnippetsFetcher(task_runner.get(), + signin_manager, token_service, request_context_getter, + base::FilePath())))); + return service; + } + private: + scoped_ptr<AccountTrackerService> account_tracker_; + scoped_ptr<TestSigninClient> signin_client_; + base::MessageLoop message_loop_; DISALLOW_COPY_AND_ASSIGN(NTPSnippetsServiceTest); }; -TEST_F(NTPSnippetsServiceTest, Create) { - std::string language_code("fr"); - scoped_ptr<ntp_snippets::NTPSnippetsService> service( - new ntp_snippets::NTPSnippetsService(language_code)); +TEST_F(NTPSnippetsServiceTest, Create) { + scoped_ptr<NTPSnippetsService> service(CreateSnippetService()); EXPECT_FALSE(service->is_loaded()); } TEST_F(NTPSnippetsServiceTest, Loop) { - std::string language_code("fr"); - scoped_ptr<ntp_snippets::NTPSnippetsService> service( - new ntp_snippets::NTPSnippetsService(language_code)); + scoped_ptr<NTPSnippetsService> service(CreateSnippetService()); EXPECT_FALSE(service->is_loaded()); @@ -71,15 +100,13 @@ TEST_F(NTPSnippetsServiceTest, Loop) { EXPECT_EQ(snippet.url(), GURL("http://localhost/foobar")); } // Without the const, this should not compile. - for (const ntp_snippets::NTPSnippet& snippet : *service) { + for (const NTPSnippet& snippet : *service) { EXPECT_EQ(snippet.url(), GURL("http://localhost/foobar")); } } TEST_F(NTPSnippetsServiceTest, Full) { - std::string language_code("fr"); - scoped_ptr<ntp_snippets::NTPSnippetsService> service( - new ntp_snippets::NTPSnippetsService(language_code)); + scoped_ptr<NTPSnippetsService> service(CreateSnippetService()); std::string json_str( "{ \"recos\": [ " @@ -112,9 +139,7 @@ TEST_F(NTPSnippetsServiceTest, Full) { } TEST_F(NTPSnippetsServiceTest, ObserverNotLoaded) { - std::string language_code("fr"); - scoped_ptr<ntp_snippets::NTPSnippetsService> service( - new ntp_snippets::NTPSnippetsService(language_code)); + scoped_ptr<NTPSnippetsService> service(CreateSnippetService()); SnippetObserver observer; service->AddObserver(&observer); @@ -131,9 +156,7 @@ TEST_F(NTPSnippetsServiceTest, ObserverNotLoaded) { } TEST_F(NTPSnippetsServiceTest, ObserverLoaded) { - std::string language_code("fr"); - scoped_ptr<ntp_snippets::NTPSnippetsService> service( - new ntp_snippets::NTPSnippetsService(language_code)); + scoped_ptr<NTPSnippetsService> service(CreateSnippetService()); std::string json_str( "{ \"recos\": [ " @@ -148,4 +171,4 @@ TEST_F(NTPSnippetsServiceTest, ObserverLoaded) { service->RemoveObserver(&observer); } -} // namespace +} // namespace ntp_snippets diff --git a/ios/chrome/browser/ntp_snippets/ios_chrome_ntp_snippets_service_factory.cc b/ios/chrome/browser/ntp_snippets/ios_chrome_ntp_snippets_service_factory.cc index 4fa922e..0602d85 100644 --- a/ios/chrome/browser/ntp_snippets/ios_chrome_ntp_snippets_service_factory.cc +++ b/ios/chrome/browser/ntp_snippets/ios_chrome_ntp_snippets_service_factory.cc @@ -7,9 +7,17 @@ #include "base/logging.h" #include "base/memory/singleton.h" #include "components/keyed_service/ios/browser_state_dependency_manager.h" +#include "components/ntp_snippets/ntp_snippets_fetcher.h" #include "components/ntp_snippets/ntp_snippets_service.h" +#include "components/signin/core/browser/profile_oauth2_token_service.h" +#include "google_apis/gaia/oauth2_token_service.h" #include "ios/chrome/browser/application_context.h" #include "ios/chrome/browser/browser_state/chrome_browser_state.h" +#include "ios/chrome/browser/signin/oauth2_token_service_factory.h" +#include "ios/chrome/browser/signin/signin_manager_factory.h" +#include "ios/web/public/browser_state.h" +#include "ios/web/public/web_thread.h" +#include "net/url_request/url_request_context_getter.h" // static IOSChromeNTPSnippetsServiceFactory* @@ -36,7 +44,24 @@ IOSChromeNTPSnippetsServiceFactory::~IOSChromeNTPSnippetsServiceFactory() {} scoped_ptr<KeyedService> IOSChromeNTPSnippetsServiceFactory::BuildServiceInstanceFor( web::BrowserState* browser_state) const { + ios::ChromeBrowserState* chrome_browser_state = + ios::ChromeBrowserState::FromBrowserState(browser_state); DCHECK(!browser_state->IsOffTheRecord()); + SigninManager* signin_manager = + ios::SigninManagerFactory::GetForBrowserState(chrome_browser_state); + OAuth2TokenService* token_service = + OAuth2TokenServiceFactory::GetForBrowserState(chrome_browser_state); + scoped_refptr<net::URLRequestContextGetter> request_context = + browser_state->GetRequestContext(); + + scoped_refptr<base::SequencedTaskRunner> task_runner = + web::WebThread::GetBlockingPool() + ->GetSequencedTaskRunnerWithShutdownBehavior( + base::SequencedWorkerPool::GetSequenceToken(), + base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN); return make_scoped_ptr(new ntp_snippets::NTPSnippetsService( - GetApplicationContext()->GetApplicationLocale())); + task_runner, GetApplicationContext()->GetApplicationLocale(), + make_scoped_ptr(new ntp_snippets::NTPSnippetsFetcher( + task_runner, (SigninManagerBase*)signin_manager, token_service, + request_context, browser_state->GetStatePath())))); } |
