1 // Copyright (c) 2012 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/ui/webui/settings/custom_home_pages_table_model.h"
6 
7 #include <stddef.h>
8 
9 #include "base/bind.h"
10 #include "base/callback_helpers.h"
11 #include "base/i18n/rtl.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/history/history_service_factory.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/browser/ui/browser.h"
16 #include "chrome/browser/ui/browser_list.h"
17 #include "chrome/browser/ui/tabs/tab_strip_model.h"
18 #include "chrome/common/url_constants.h"
19 #include "chrome/grit/generated_resources.h"
20 #include "components/history/core/browser/history_service.h"
21 #include "components/url_formatter/url_formatter.h"
22 #include "content/public/browser/web_contents.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/base/models/table_model_observer.h"
25 #include "ui/gfx/codec/png_codec.h"
26 #include "url/gurl.h"
27 
28 #if defined(OS_CHROMEOS)
29 #include "chrome/browser/ui/settings_window_manager_chromeos.h"
30 #endif
31 
32 struct CustomHomePagesTableModel::Entry {
EntryCustomHomePagesTableModel::Entry33   Entry() : task_id(base::CancelableTaskTracker::kBadTaskId) {}
34 
35   // URL of the page.
36   GURL url;
37 
38   // Page title.  If this is empty, we'll display the URL as the entry.
39   base::string16 title;
40 
41   // If not |base::CancelableTaskTracker::kBadTaskId|, indicates we're loading
42   // the title for the page.
43   base::CancelableTaskTracker::TaskId task_id;
44 };
45 
CustomHomePagesTableModel(Profile * profile)46 CustomHomePagesTableModel::CustomHomePagesTableModel(Profile* profile)
47     : profile_(profile),
48       observer_(nullptr),
49       num_outstanding_title_lookups_(0) {}
50 
~CustomHomePagesTableModel()51 CustomHomePagesTableModel::~CustomHomePagesTableModel() {
52 }
53 
SetURLs(const std::vector<GURL> & urls)54 void CustomHomePagesTableModel::SetURLs(const std::vector<GURL>& urls) {
55   entries_.resize(urls.size());
56   for (size_t i = 0; i < urls.size(); ++i) {
57     entries_[i].url = urls[i];
58     entries_[i].title.erase();
59   }
60   LoadAllTitles();
61 }
62 
63 /**
64  * Move a number of existing entries to a new position, reordering the table.
65  *
66  * We determine the range of elements affected by the move, save the moved
67  * elements, compact the remaining ones, and re-insert moved elements.
68  * Expects |index_list| to be ordered ascending.
69  */
MoveURLs(int insert_before,const std::vector<int> & index_list)70 void CustomHomePagesTableModel::MoveURLs(int insert_before,
71                                          const std::vector<int>& index_list) {
72   if (index_list.empty()) return;
73   DCHECK(insert_before >= 0 && insert_before <= RowCount());
74 
75   // The range of elements that needs to be reshuffled is [ |first|, |last| ).
76   int first = std::min(insert_before, index_list.front());
77   int last = std::max(insert_before, index_list.back() + 1);
78 
79   // Save the dragged elements. Also, adjust insertion point if it is before a
80   // dragged element.
81   std::vector<Entry> moved_entries;
82   for (size_t i = 0; i < index_list.size(); ++i) {
83     moved_entries.push_back(entries_[index_list[i]]);
84     if (index_list[i] == insert_before)
85       insert_before++;
86   }
87 
88   // Compact the range between beginning and insertion point, moving downwards.
89   size_t skip_count = 0;
90   for (int i = first; i < insert_before; ++i) {
91     if (skip_count < index_list.size() && index_list[skip_count] == i)
92       skip_count++;
93     else
94       entries_[i - skip_count] = entries_[i];
95   }
96 
97   // Moving items down created a gap. We start compacting up after it.
98   first = insert_before;
99   insert_before -= skip_count;
100 
101   // Now compact up for elements after the insertion point.
102   skip_count = 0;
103   for (int i = last - 1; i >= first; --i) {
104     if (skip_count < index_list.size() &&
105         index_list[index_list.size() - skip_count - 1] == i) {
106       skip_count++;
107     } else {
108       entries_[i + skip_count] = entries_[i];
109     }
110   }
111 
112   // Insert moved elements.
113   std::copy(moved_entries.begin(), moved_entries.end(),
114       entries_.begin() + insert_before);
115 
116   // Possibly large change, so tell the view to just rebuild itself.
117   if (observer_)
118     observer_->OnModelChanged();
119 }
120 
AddWithoutNotification(int index,const GURL & url)121 void CustomHomePagesTableModel::AddWithoutNotification(
122     int index, const GURL& url) {
123   DCHECK(index >= 0 && index <= RowCount());
124   entries_.insert(entries_.begin() + static_cast<size_t>(index), Entry());
125   entries_[index].url = url;
126 }
127 
Add(int index,const GURL & url)128 void CustomHomePagesTableModel::Add(int index, const GURL& url) {
129   AddWithoutNotification(index, url);
130   LoadTitle(&(entries_[index]));
131   if (observer_)
132     observer_->OnItemsAdded(index, 1);
133 }
134 
RemoveWithoutNotification(int index)135 void CustomHomePagesTableModel::RemoveWithoutNotification(int index) {
136   DCHECK(index >= 0 && index < RowCount());
137   Entry* entry = &(entries_[index]);
138   // Cancel any pending load requests now so we don't deref a bogus pointer when
139   // we get the loaded notification.
140   if (entry->task_id != base::CancelableTaskTracker::kBadTaskId) {
141     task_tracker_.TryCancel(entry->task_id);
142     entry->task_id = base::CancelableTaskTracker::kBadTaskId;
143   }
144   entries_.erase(entries_.begin() + static_cast<size_t>(index));
145 }
146 
Remove(int index)147 void CustomHomePagesTableModel::Remove(int index) {
148   RemoveWithoutNotification(index);
149   if (observer_)
150     observer_->OnItemsRemoved(index, 1);
151 }
152 
SetToCurrentlyOpenPages(content::WebContents * ignore_contents)153 void CustomHomePagesTableModel::SetToCurrentlyOpenPages(
154     content::WebContents* ignore_contents) {
155   // Remove the current entries.
156   while (RowCount())
157     RemoveWithoutNotification(0);
158 
159   // Add tabs from appropriate browser windows.
160   int add_index = 0;
161   for (auto* browser : *BrowserList::GetInstance()) {
162     if (!ShouldIncludeBrowser(browser))
163       continue;
164 
165     for (int tab_index = 0;
166          tab_index < browser->tab_strip_model()->count();
167          ++tab_index) {
168       content::WebContents* contents =
169           browser->tab_strip_model()->GetWebContentsAt(tab_index);
170       if (contents == ignore_contents)
171         continue;
172       const GURL url = contents->GetURL();
173       if (!url.is_empty() && !url.SchemeIs(content::kChromeDevToolsScheme))
174         AddWithoutNotification(add_index++, url);
175     }
176   }
177   LoadAllTitles();
178 }
179 
GetURLs()180 std::vector<GURL> CustomHomePagesTableModel::GetURLs() {
181   std::vector<GURL> urls(entries_.size());
182   for (size_t i = 0; i < entries_.size(); ++i)
183     urls[i] = entries_[i].url;
184   return urls;
185 }
186 
RowCount()187 int CustomHomePagesTableModel::RowCount() {
188   return static_cast<int>(entries_.size());
189 }
190 
GetText(int row,int column_id)191 base::string16 CustomHomePagesTableModel::GetText(int row, int column_id) {
192   DCHECK(column_id == 0);
193   DCHECK(row >= 0 && row < RowCount());
194   return entries_[row].title.empty() ? FormattedURL(row) : entries_[row].title;
195 }
196 
GetTooltip(int row)197 base::string16 CustomHomePagesTableModel::GetTooltip(int row) {
198   return entries_[row].title.empty()
199              ? base::string16()
200              : l10n_util::GetStringFUTF16(IDS_SETTINGS_ON_STARTUP_PAGE_TOOLTIP,
201                                           entries_[row].title,
202                                           FormattedURL(row));
203 }
204 
SetObserver(ui::TableModelObserver * observer)205 void CustomHomePagesTableModel::SetObserver(ui::TableModelObserver* observer) {
206   observer_ = observer;
207 }
208 
ShouldIncludeBrowser(Browser * browser)209 bool CustomHomePagesTableModel::ShouldIncludeBrowser(Browser* browser) {
210   // Do not include incognito browsers.
211   if (browser->profile() != profile_)
212     return false;
213 #if defined(OS_CHROMEOS)
214   // Do not include the Settings window.
215   if (chrome::SettingsWindowManager::GetInstance()->IsSettingsBrowser(
216           browser)) {
217     return false;
218   }
219 #endif
220   return true;
221 }
222 
LoadTitle(Entry * entry)223 void CustomHomePagesTableModel::LoadTitle(Entry* entry) {
224   history::HistoryService* history_service =
225       HistoryServiceFactory::GetForProfile(profile_,
226                                            ServiceAccessType::EXPLICIT_ACCESS);
227   if (history_service) {
228     entry->task_id = history_service->QueryURL(
229         entry->url, false,
230         base::BindOnce(&CustomHomePagesTableModel::OnGotTitle,
231                        base::Unretained(this), entry->url, false),
232         &task_tracker_);
233   }
234 }
235 
LoadAllTitles()236 void CustomHomePagesTableModel::LoadAllTitles() {
237   history::HistoryService* history_service =
238       HistoryServiceFactory::GetForProfile(profile_,
239                                            ServiceAccessType::EXPLICIT_ACCESS);
240   // It's possible for multiple LoadAllTitles() queries to be inflight we want
241   // to make sure everything is resolved before updating the observer or we risk
242   // getting rendering glitches.
243   num_outstanding_title_lookups_ += entries_.size();
244   for (Entry& entry : entries_) {
245     if (history_service) {
246       entry.task_id = history_service->QueryURL(
247           entry.url, false,
248           base::BindOnce(&CustomHomePagesTableModel::OnGotOneOfManyTitles,
249                          base::Unretained(this), entry.url),
250           &task_tracker_);
251     }
252   }
253   if (entries_.empty())
254     observer_->OnModelChanged();
255 }
256 
OnGotOneOfManyTitles(const GURL & entry_url,history::QueryURLResult result)257 void CustomHomePagesTableModel::OnGotOneOfManyTitles(
258     const GURL& entry_url,
259     history::QueryURLResult result) {
260   OnGotTitle(entry_url, false, std::move(result));
261   DCHECK_GE(num_outstanding_title_lookups_, 1);
262   if (--num_outstanding_title_lookups_ == 0 && observer_)
263     observer_->OnModelChanged();
264 }
265 
OnGotTitle(const GURL & entry_url,bool observable,history::QueryURLResult result)266 void CustomHomePagesTableModel::OnGotTitle(const GURL& entry_url,
267                                            bool observable,
268                                            history::QueryURLResult result) {
269   Entry* entry = NULL;
270   size_t entry_index = 0;
271   for (size_t i = 0; i < entries_.size(); ++i) {
272     if (entries_[i].url == entry_url) {
273       entry = &entries_[i];
274       entry_index = i;
275       break;
276     }
277   }
278   if (!entry) {
279     // The URLs changed before we were called back.
280     return;
281   }
282   entry->task_id = base::CancelableTaskTracker::kBadTaskId;
283   if (result.success && !result.row.title().empty()) {
284     entry->title = result.row.title();
285     if (observer_ && observable)
286       observer_->OnItemsChanged(static_cast<int>(entry_index), 1);
287   }
288 }
289 
FormattedURL(int row) const290 base::string16 CustomHomePagesTableModel::FormattedURL(int row) const {
291   base::string16 url = url_formatter::FormatUrl(entries_[row].url);
292   url = base::i18n::GetDisplayStringInLTRDirectionality(url);
293   return url;
294 }
295