1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/android/most_visited_sites.h"
6
7#include <string>
8#include <vector>
9
10#include "base/android/jni_android.h"
11#include "base/android/jni_array.h"
12#include "base/android/jni_string.h"
13#include "base/android/scoped_java_ref.h"
14#include "base/callback.h"
15#include "base/metrics/histogram.h"
16#include "base/metrics/sparse_histogram.h"
17#include "base/strings/string_number_conversions.h"
18#include "base/strings/stringprintf.h"
19#include "base/strings/utf_string_conversions.h"
20#include "base/time/time.h"
21#include "chrome/browser/chrome_notification_types.h"
22#include "chrome/browser/history/top_sites.h"
23#include "chrome/browser/profiles/profile.h"
24#include "chrome/browser/profiles/profile_android.h"
25#include "chrome/browser/search/suggestions/suggestions_service_factory.h"
26#include "chrome/browser/search/suggestions/suggestions_source.h"
27#include "chrome/browser/sync/profile_sync_service.h"
28#include "chrome/browser/sync/profile_sync_service_factory.h"
29#include "chrome/browser/thumbnails/thumbnail_list_source.h"
30#include "components/suggestions/suggestions_service.h"
31#include "components/suggestions/suggestions_utils.h"
32#include "content/public/browser/browser_thread.h"
33#include "content/public/browser/notification_source.h"
34#include "content/public/browser/url_data_source.h"
35#include "jni/MostVisitedSites_jni.h"
36#include "third_party/skia/include/core/SkBitmap.h"
37#include "ui/gfx/android/java_bitmap.h"
38#include "ui/gfx/codec/jpeg_codec.h"
39
40using base::android::AttachCurrentThread;
41using base::android::ConvertUTF8ToJavaString;
42using base::android::ConvertJavaStringToUTF8;
43using base::android::ScopedJavaGlobalRef;
44using base::android::ToJavaArrayOfStrings;
45using base::android::CheckException;
46using content::BrowserThread;
47using history::TopSites;
48using suggestions::ChromeSuggestion;
49using suggestions::SuggestionsProfile;
50using suggestions::SuggestionsService;
51using suggestions::SuggestionsServiceFactory;
52using suggestions::SyncState;
53
54namespace {
55
56// Total number of tiles displayed.
57const char kNumTilesHistogramName[] = "NewTabPage.NumberOfTiles";
58// Tracking thumbnails.
59const char kNumLocalThumbnailTilesHistogramName[] =
60    "NewTabPage.NumberOfThumbnailTiles";
61const char kNumEmptyTilesHistogramName[] = "NewTabPage.NumberOfGrayTiles";
62const char kNumServerTilesHistogramName[] = "NewTabPage.NumberOfExternalTiles";
63// Client suggestion opened.
64const char kOpenedItemClientHistogramName[] = "NewTabPage.MostVisited.client";
65// Control group suggestion opened.
66const char kOpenedItemControlHistogramName[] = "NewTabPage.MostVisited.client0";
67// Server suggestion opened, no provider.
68const char kOpenedItemServerHistogramName[] = "NewTabPage.MostVisited.server";
69// Server suggestion opened with provider.
70const char kOpenedItemServerProviderHistogramFormat[] =
71    "NewTabPage.MostVisited.server%d";
72// Client impression.
73const char kImpressionClientHistogramName[] =
74    "NewTabPage.SuggestionsImpression.client";
75// Control group impression.
76const char kImpressionControlHistogramName[] =
77    "NewTabPage.SuggestionsImpression.client0";
78// Server suggestion impression, no provider.
79const char kImpressionServerHistogramName[] =
80    "NewTabPage.SuggestionsImpression.server";
81// Server suggestion impression with provider.
82const char kImpressionServerHistogramFormat[] =
83    "NewTabPage.SuggestionsImpression.server%d";
84
85void ExtractMostVisitedTitlesAndURLs(
86    const history::MostVisitedURLList& visited_list,
87    std::vector<base::string16>* titles,
88    std::vector<std::string>* urls,
89    int num_sites) {
90  size_t max = static_cast<size_t>(num_sites);
91  for (size_t i = 0; i < visited_list.size() && i < max; ++i) {
92    const history::MostVisitedURL& visited = visited_list[i];
93
94    if (visited.url.is_empty())
95      break;  // This is the signal that there are no more real visited sites.
96
97    titles->push_back(visited.title);
98    urls->push_back(visited.url.spec());
99  }
100}
101
102SkBitmap ExtractThumbnail(const base::RefCountedMemory& image_data) {
103  scoped_ptr<SkBitmap> image(gfx::JPEGCodec::Decode(
104      image_data.front(),
105      image_data.size()));
106  return image.get() ? *image : SkBitmap();
107}
108
109void AddForcedURLOnUIThread(scoped_refptr<history::TopSites> top_sites,
110                            const GURL& url) {
111  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
112  top_sites->AddForcedURL(url, base::Time::Now());
113}
114
115// Runs on the DB thread.
116void GetUrlThumbnailTask(
117    std::string url_string,
118    scoped_refptr<TopSites> top_sites,
119    ScopedJavaGlobalRef<jobject>* j_callback,
120    MostVisitedSites::LookupSuccessCallback lookup_success_ui_callback,
121    base::Closure lookup_failed_ui_callback) {
122  JNIEnv* env = AttachCurrentThread();
123
124  ScopedJavaGlobalRef<jobject>* j_bitmap_ref =
125      new ScopedJavaGlobalRef<jobject>();
126
127  GURL gurl(url_string);
128
129  scoped_refptr<base::RefCountedMemory> data;
130  if (top_sites->GetPageThumbnail(gurl, false, &data)) {
131    SkBitmap thumbnail_bitmap = ExtractThumbnail(*data.get());
132    if (!thumbnail_bitmap.empty()) {
133      j_bitmap_ref->Reset(
134          env,
135          gfx::ConvertToJavaBitmap(&thumbnail_bitmap).obj());
136    }
137  } else {
138    // A thumbnail is not locally available for |gurl|. Make sure it is put in
139    // the list to be fetched at the next visit to this site.
140    BrowserThread::PostTask(
141        BrowserThread::UI, FROM_HERE,
142        base::Bind(AddForcedURLOnUIThread, top_sites, gurl));
143
144    // If appropriate, return on the UI thread to execute the proper callback.
145    if (!lookup_failed_ui_callback.is_null()) {
146      BrowserThread::PostTask(
147          BrowserThread::UI, FROM_HERE, lookup_failed_ui_callback);
148      delete j_bitmap_ref;
149      return;
150    }
151  }
152
153  // Since j_callback is owned by this callback, when the callback falls out of
154  // scope it will be deleted. We need to pass ownership to the next callback.
155  ScopedJavaGlobalRef<jobject>* j_callback_pass =
156      new ScopedJavaGlobalRef<jobject>(*j_callback);
157  BrowserThread::PostTask(
158      BrowserThread::UI, FROM_HERE,
159      base::Bind(lookup_success_ui_callback, base::Owned(j_bitmap_ref),
160                 base::Owned(j_callback_pass)));
161}
162
163// Log an event for a given |histogram| at a given element |position|. This
164// routine exists because regular histogram macros are cached thus can't be used
165// if the name of the histogram will change at a given call site.
166void LogHistogramEvent(const std::string& histogram, int position,
167                       int num_sites) {
168  base::HistogramBase* counter = base::LinearHistogram::FactoryGet(
169      histogram,
170      1,
171      num_sites,
172      num_sites + 1,
173      base::Histogram::kUmaTargetedHistogramFlag);
174  if (counter)
175    counter->Add(position);
176}
177
178// Return the current SyncState for use with the SuggestionsService.
179SyncState GetSyncState(Profile* profile) {
180  ProfileSyncService* sync =
181      ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile);
182  if (!sync)
183    return SyncState::SYNC_OR_HISTORY_SYNC_DISABLED;
184  return suggestions::GetSyncState(
185      sync->IsSyncEnabledAndLoggedIn(),
186      sync->sync_initialized(),
187      sync->GetActiveDataTypes().Has(syncer::HISTORY_DELETE_DIRECTIVES));
188}
189
190}  // namespace
191
192MostVisitedSites::MostVisitedSites(Profile* profile)
193    : profile_(profile), num_sites_(0), is_control_group_(false),
194      num_local_thumbs_(0), num_server_thumbs_(0), num_empty_thumbs_(0),
195      weak_ptr_factory_(this) {
196  // Register the debugging page for the Suggestions Service and the thumbnails
197  // debugging page.
198  content::URLDataSource::Add(profile_,
199                              new suggestions::SuggestionsSource(profile_));
200  content::URLDataSource::Add(profile_, new ThumbnailListSource(profile_));
201
202  // Register this class as an observer to the sync service. It is important to
203  // be notified of changes in the sync state such as initialization, sync
204  // being enabled or disabled, etc.
205  ProfileSyncService* profile_sync_service =
206      ProfileSyncServiceFactory::GetForProfile(profile_);
207  if (profile_sync_service)
208    profile_sync_service->AddObserver(this);
209}
210
211MostVisitedSites::~MostVisitedSites() {
212  ProfileSyncService* profile_sync_service =
213      ProfileSyncServiceFactory::GetForProfile(profile_);
214  if (profile_sync_service && profile_sync_service->HasObserver(this))
215    profile_sync_service->RemoveObserver(this);
216}
217
218void MostVisitedSites::Destroy(JNIEnv* env, jobject obj) {
219  delete this;
220}
221
222void MostVisitedSites::OnLoadingComplete(JNIEnv* env, jobject obj) {
223  RecordUMAMetrics();
224}
225
226void MostVisitedSites::SetMostVisitedURLsObserver(JNIEnv* env,
227                                                  jobject obj,
228                                                  jobject j_observer,
229                                                  jint num_sites) {
230  observer_.Reset(env, j_observer);
231  num_sites_ = num_sites;
232
233  QueryMostVisitedURLs();
234
235  history::TopSites* top_sites = profile_->GetTopSites();
236  if (top_sites) {
237    // TopSites updates itself after a delay. To ensure up-to-date results,
238    // force an update now.
239    top_sites->SyncWithHistory();
240
241    // Register for notification when TopSites changes so that we can update
242    // ourself.
243    registrar_.Add(this, chrome::NOTIFICATION_TOP_SITES_CHANGED,
244                   content::Source<history::TopSites>(top_sites));
245  }
246}
247
248// Called from the UI Thread.
249void MostVisitedSites::GetURLThumbnail(JNIEnv* env,
250                                       jobject obj,
251                                       jstring url,
252                                       jobject j_callback_obj) {
253  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
254  ScopedJavaGlobalRef<jobject>* j_callback =
255      new ScopedJavaGlobalRef<jobject>();
256  j_callback->Reset(env, j_callback_obj);
257
258  std::string url_string = ConvertJavaStringToUTF8(env, url);
259  scoped_refptr<TopSites> top_sites(profile_->GetTopSites());
260
261  // If the Suggestions service is enabled and in use, create a callback to
262  // fetch a server thumbnail from it, in case the local thumbnail is not found.
263  SuggestionsService* suggestions_service =
264      SuggestionsServiceFactory::GetForProfile(profile_);
265  bool use_suggestions_service = suggestions_service &&
266      mv_source_ == SUGGESTIONS_SERVICE;
267  base::Closure lookup_failed_callback = use_suggestions_service ?
268      base::Bind(&MostVisitedSites::GetSuggestionsThumbnailOnUIThread,
269                 weak_ptr_factory_.GetWeakPtr(),
270                 suggestions_service, url_string,
271                 base::Owned(new ScopedJavaGlobalRef<jobject>(*j_callback))) :
272      base::Closure();
273  LookupSuccessCallback lookup_success_callback =
274      base::Bind(&MostVisitedSites::OnObtainedThumbnail,
275                 weak_ptr_factory_.GetWeakPtr());
276
277  BrowserThread::PostTask(
278      BrowserThread::DB, FROM_HERE,
279          base::Bind(
280              &GetUrlThumbnailTask, url_string, top_sites,
281              base::Owned(j_callback), lookup_success_callback,
282              lookup_failed_callback));
283}
284
285void MostVisitedSites::BlacklistUrl(JNIEnv* env,
286                                    jobject obj,
287                                    jstring j_url) {
288  std::string url = ConvertJavaStringToUTF8(env, j_url);
289
290  switch (mv_source_) {
291    case TOP_SITES: {
292      TopSites* top_sites = profile_->GetTopSites();
293      DCHECK(top_sites);
294      top_sites->AddBlacklistedURL(GURL(url));
295      break;
296    }
297
298    case SUGGESTIONS_SERVICE: {
299      SuggestionsService* suggestions_service =
300          SuggestionsServiceFactory::GetForProfile(profile_);
301      DCHECK(suggestions_service);
302      suggestions_service->BlacklistURL(
303          GURL(url),
304          base::Bind(
305              &MostVisitedSites::OnSuggestionsProfileAvailable,
306              weak_ptr_factory_.GetWeakPtr(),
307              base::Owned(new ScopedJavaGlobalRef<jobject>(observer_))));
308      break;
309    }
310  }
311}
312
313void MostVisitedSites::RecordOpenedMostVisitedItem(JNIEnv* env,
314                                                   jobject obj,
315                                                   jint index) {
316  switch (mv_source_) {
317    case TOP_SITES: {
318      const std::string histogram = is_control_group_ ?
319          kOpenedItemControlHistogramName : kOpenedItemClientHistogramName;
320      LogHistogramEvent(histogram, index, num_sites_);
321      break;
322    }
323    case SUGGESTIONS_SERVICE: {
324      if (server_suggestions_.suggestions_size() > index) {
325        if (server_suggestions_.suggestions(index).providers_size()) {
326          std::string histogram = base::StringPrintf(
327              kOpenedItemServerProviderHistogramFormat,
328              server_suggestions_.suggestions(index).providers(0));
329          LogHistogramEvent(histogram, index, num_sites_);
330        } else {
331          UMA_HISTOGRAM_SPARSE_SLOWLY(kOpenedItemServerHistogramName, index);
332        }
333      }
334      break;
335    }
336  }
337}
338
339void MostVisitedSites::Observe(int type,
340                               const content::NotificationSource& source,
341                               const content::NotificationDetails& details) {
342  DCHECK_EQ(type, chrome::NOTIFICATION_TOP_SITES_CHANGED);
343
344  if (mv_source_ == TOP_SITES) {
345    // The displayed suggestions are invalidated.
346    QueryMostVisitedURLs();
347  }
348}
349
350void MostVisitedSites::OnStateChanged() {
351  // There have been changes to the sync state. This class cares about a few
352  // (just initialized, enabled/disabled or history sync state changed). Re-run
353  // the query code which will use the proper state.
354  QueryMostVisitedURLs();
355}
356
357// static
358bool MostVisitedSites::Register(JNIEnv* env) {
359  return RegisterNativesImpl(env);
360}
361
362void MostVisitedSites::QueryMostVisitedURLs() {
363  SuggestionsService* suggestions_service =
364      SuggestionsServiceFactory::GetForProfile(profile_);
365  if (suggestions_service) {
366    // Suggestions service is enabled, initiate a query.
367    suggestions_service->FetchSuggestionsData(
368        GetSyncState(profile_),
369        base::Bind(
370          &MostVisitedSites::OnSuggestionsProfileAvailable,
371          weak_ptr_factory_.GetWeakPtr(),
372          base::Owned(new ScopedJavaGlobalRef<jobject>(observer_))));
373  } else {
374    InitiateTopSitesQuery();
375  }
376}
377
378void MostVisitedSites::InitiateTopSitesQuery() {
379  TopSites* top_sites = profile_->GetTopSites();
380  if (!top_sites)
381    return;
382
383  top_sites->GetMostVisitedURLs(
384      base::Bind(
385          &MostVisitedSites::OnMostVisitedURLsAvailable,
386          weak_ptr_factory_.GetWeakPtr(),
387          base::Owned(new ScopedJavaGlobalRef<jobject>(observer_)),
388          num_sites_),
389      false);
390}
391
392void MostVisitedSites::OnMostVisitedURLsAvailable(
393    ScopedJavaGlobalRef<jobject>* j_observer,
394    int num_sites,
395    const history::MostVisitedURLList& visited_list) {
396  std::vector<base::string16> titles;
397  std::vector<std::string> urls;
398  ExtractMostVisitedTitlesAndURLs(visited_list, &titles, &urls, num_sites);
399
400  mv_source_ = TOP_SITES;
401
402  int num_tiles = urls.size();
403  UMA_HISTOGRAM_SPARSE_SLOWLY(kNumTilesHistogramName, num_tiles);
404  const std::string histogram = is_control_group_ ?
405      kImpressionControlHistogramName : kImpressionClientHistogramName;
406  for (int i = 0; i < num_tiles; ++i) {
407    LogHistogramEvent(histogram, i, num_sites_);
408  }
409
410  JNIEnv* env = AttachCurrentThread();
411  Java_MostVisitedURLsObserver_onMostVisitedURLsAvailable(
412      env,
413      j_observer->obj(),
414      ToJavaArrayOfStrings(env, titles).obj(),
415      ToJavaArrayOfStrings(env, urls).obj());
416}
417
418void MostVisitedSites::OnSuggestionsProfileAvailable(
419    ScopedJavaGlobalRef<jobject>* j_observer,
420    const SuggestionsProfile& suggestions_profile) {
421  int size = suggestions_profile.suggestions_size();
422
423  // Determine if the user is in a control group (they would have received
424  // suggestions, but are in a group where they shouldn't).
425  is_control_group_ = size && SuggestionsService::IsControlGroup();
426
427  // If no suggestions data is available or the user is in a control group,
428  // initiate Top Sites query.
429  if (is_control_group_ || !size) {
430    InitiateTopSitesQuery();
431    return;
432  }
433
434  std::vector<base::string16> titles;
435  std::vector<std::string> urls;
436
437  int i = 0;
438  for (; i < size && i < num_sites_; ++i) {
439    const ChromeSuggestion& suggestion = suggestions_profile.suggestions(i);
440    titles.push_back(base::UTF8ToUTF16(suggestion.title()));
441    urls.push_back(suggestion.url());
442    if (suggestion.providers_size()) {
443      std::string histogram = base::StringPrintf(
444          kImpressionServerHistogramFormat, suggestion.providers(0));
445      LogHistogramEvent(histogram, i, num_sites_);
446    } else {
447      UMA_HISTOGRAM_SPARSE_SLOWLY(kImpressionServerHistogramName, i);
448    }
449  }
450  UMA_HISTOGRAM_SPARSE_SLOWLY(kNumTilesHistogramName, i);
451
452  mv_source_ = SUGGESTIONS_SERVICE;
453  // Keep a copy of the suggestions for eventual logging.
454  server_suggestions_ = suggestions_profile;
455
456  JNIEnv* env = AttachCurrentThread();
457  Java_MostVisitedURLsObserver_onMostVisitedURLsAvailable(
458      env,
459      j_observer->obj(),
460      ToJavaArrayOfStrings(env, titles).obj(),
461      ToJavaArrayOfStrings(env, urls).obj());
462}
463
464void MostVisitedSites::OnObtainedThumbnail(
465    ScopedJavaGlobalRef<jobject>* bitmap,
466    ScopedJavaGlobalRef<jobject>* j_callback) {
467  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
468  JNIEnv* env = AttachCurrentThread();
469  if (bitmap->obj()) {
470    num_local_thumbs_++;
471  } else {
472    num_empty_thumbs_++;
473  }
474  Java_ThumbnailCallback_onMostVisitedURLsThumbnailAvailable(
475      env, j_callback->obj(), bitmap->obj());
476}
477
478void MostVisitedSites::GetSuggestionsThumbnailOnUIThread(
479    SuggestionsService* suggestions_service,
480    const std::string& url_string,
481    ScopedJavaGlobalRef<jobject>* j_callback) {
482  suggestions_service->GetPageThumbnail(
483      GURL(url_string),
484      base::Bind(&MostVisitedSites::OnSuggestionsThumbnailAvailable,
485                 weak_ptr_factory_.GetWeakPtr(),
486                 base::Owned(new ScopedJavaGlobalRef<jobject>(*j_callback))));
487}
488
489void MostVisitedSites::OnSuggestionsThumbnailAvailable(
490    ScopedJavaGlobalRef<jobject>* j_callback,
491    const GURL& url,
492    const SkBitmap* bitmap) {
493  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
494  JNIEnv* env = AttachCurrentThread();
495
496  ScopedJavaGlobalRef<jobject>* j_bitmap_ref =
497      new ScopedJavaGlobalRef<jobject>();
498  if (bitmap) {
499    num_server_thumbs_++;
500    j_bitmap_ref->Reset(
501        env,
502        gfx::ConvertToJavaBitmap(bitmap).obj());
503  } else {
504    num_empty_thumbs_++;
505  }
506
507  Java_ThumbnailCallback_onMostVisitedURLsThumbnailAvailable(
508      env, j_callback->obj(), j_bitmap_ref->obj());
509}
510
511void MostVisitedSites::RecordUMAMetrics() {
512  UMA_HISTOGRAM_SPARSE_SLOWLY(kNumLocalThumbnailTilesHistogramName,
513                              num_local_thumbs_);
514  num_local_thumbs_ = 0;
515  UMA_HISTOGRAM_SPARSE_SLOWLY(kNumEmptyTilesHistogramName, num_empty_thumbs_);
516  num_empty_thumbs_ = 0;
517  UMA_HISTOGRAM_SPARSE_SLOWLY(kNumServerTilesHistogramName, num_server_thumbs_);
518  num_server_thumbs_ = 0;
519}
520
521static jlong Init(JNIEnv* env, jobject obj, jobject jprofile) {
522  MostVisitedSites* most_visited_sites =
523      new MostVisitedSites(ProfileAndroid::FromProfileAndroid(jprofile));
524  return reinterpret_cast<intptr_t>(most_visited_sites);
525}
526