1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 package org.mozilla.gecko.home;
7 
8 import java.util.concurrent.Future;
9 
10 import android.content.Context;
11 import android.database.Cursor;
12 import android.support.annotation.NonNull;
13 import android.support.annotation.Nullable;
14 import android.support.v4.view.ViewCompat;
15 import android.support.v4.widget.TextViewCompat;
16 import android.text.Spannable;
17 import android.text.TextUtils;
18 import android.util.AttributeSet;
19 import android.view.Gravity;
20 import android.view.LayoutInflater;
21 import android.view.View;
22 import android.widget.ImageView;
23 
24 import org.mozilla.gecko.R;
25 import org.mozilla.gecko.Tab;
26 import org.mozilla.gecko.Tabs;
27 import org.mozilla.gecko.db.BrowserContract;
28 import org.mozilla.gecko.db.BrowserContract.Combined;
29 import org.mozilla.gecko.db.BrowserContract.URLColumns;
30 import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
31 import org.mozilla.gecko.icons.IconDescriptor;
32 import org.mozilla.gecko.icons.IconResponse;
33 import org.mozilla.gecko.icons.Icons;
34 import org.mozilla.gecko.reader.ReaderModeUtils;
35 import org.mozilla.gecko.reader.SavedReaderViewHelper;
36 import org.mozilla.gecko.widget.FaviconView;
37 import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
38 import org.mozilla.gecko.widget.themed.ThemedTextView;
39 
40 public class TwoLinePageRow extends ThemedLinearLayout
41                             implements Tabs.OnTabsChangedListener {
42 
43     protected static final int NO_ICON = 0;
44 
45     private final ThemedTextView mTitle;
46     private final ThemedTextView mUrl;
47     private final ImageView mStatusIcon;
48 
49     private int mSwitchToTabIconId;
50 
51     private final FaviconView mFavicon;
52     private Future<IconResponse> mOngoingIconLoad;
53 
54     private boolean mShowIcons;
55 
56     // The URL for the page corresponding to this view.
57     private String mPageUrl;
58 
59     private boolean mHasReaderCacheItem;
60 
61     private TitleFormatter mTitleFormatter;
62 
TwoLinePageRow(Context context)63     public TwoLinePageRow(Context context) {
64         this(context, null);
65     }
66 
TwoLinePageRow(Context context, AttributeSet attrs)67     public TwoLinePageRow(Context context, AttributeSet attrs) {
68         super(context, attrs);
69 
70         setGravity(Gravity.CENTER_VERTICAL);
71 
72         LayoutInflater.from(context).inflate(R.layout.two_line_page_row, this);
73 
74         mTitle = (ThemedTextView) findViewById(R.id.title);
75         mUrl = (ThemedTextView) findViewById(R.id.url);
76         mStatusIcon = (ImageView) findViewById(R.id.status_icon_bookmark);
77 
78         mSwitchToTabIconId = NO_ICON;
79         mShowIcons = true;
80 
81         mFavicon = (FaviconView) findViewById(R.id.icon);
82     }
83 
84     @Override
onAttachedToWindow()85     protected void onAttachedToWindow() {
86         super.onAttachedToWindow();
87 
88         Tabs.registerOnTabsChangedListener(this);
89     }
90 
91     @Override
onDetachedFromWindow()92     protected void onDetachedFromWindow() {
93         super.onDetachedFromWindow();
94 
95         // Tabs' listener array is safe to modify during use: its
96         // iteration pattern is based on snapshots.
97         Tabs.unregisterOnTabsChangedListener(this);
98     }
99 
100     /**
101      * Update the row in response to a tab change event.
102      * <p>
103      * This method is always invoked on the UI thread.
104      */
105     @Override
onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data)106     public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) {
107         // Carefully check if this tab event is relevant to this row.
108         final String pageUrl = mPageUrl;
109         if (pageUrl == null) {
110             return;
111         }
112         if (tab == null) {
113             return;
114         }
115 
116         // Return early if the page URL doesn't match the current tab URL,
117         // or the old tab URL.
118         // data is an empty String for ADDED/CLOSED, and contains the previous/old URL during
119         // LOCATION_CHANGE (the new URL is retrieved using tab.getURL()).
120         // tabURL and data may be about:reader URLs if the current or old tab page was a reader view
121         // page, however pageUrl will always be a plain URL (i.e. we only add about:reader when opening
122         // a reader view bookmark, at all other times it's a normal bookmark with normal URL).
123         final String tabUrl = tab.getURL();
124         if (!pageUrl.equals(ReaderModeUtils.stripAboutReaderUrl(tabUrl)) &&
125             !pageUrl.equals(ReaderModeUtils.stripAboutReaderUrl(data))) {
126             return;
127         }
128 
129         // Note: we *might* need to update the display status (i.e. switch-to-tab icon/label) if
130         // a matching tab has been opened/closed/switched to a different page. updateDisplayedUrl() will
131         // determine the changes (if any) that actually need to be made. A tab change with a matching URL
132         // does not imply that any changes are needed - e.g. if a given URL is already open in one tab, and
133         // is also opened in a second tab, the switch-to-tab status doesn't change, closing 1 of 2 tabs with a URL
134         // similarly doesn't change the switch-to-tab display, etc. (However closing the last tab for
135         // a given URL does require a status change, as does opening the first tab with that URL.)
136         switch (msg) {
137             case ADDED:
138             case CLOSED:
139             case LOCATION_CHANGE:
140                 updateDisplayedUrl();
141                 break;
142             default:
143                 break;
144         }
145     }
146 
setTitle(CharSequence text)147     private void setTitle(CharSequence text) {
148         mTitle.setText(text);
149     }
150 
setUrl(String text)151     protected void setUrl(String text) {
152         mUrl.setText(text);
153     }
154 
setUrl(int stringId)155     protected void setUrl(int stringId) {
156         mUrl.setText(stringId);
157     }
158 
getUrl()159     protected String getUrl() {
160         return mPageUrl;
161     }
162 
setSwitchToTabIcon(int iconId)163     protected void setSwitchToTabIcon(int iconId) {
164         if (mSwitchToTabIconId == iconId) {
165             return;
166         }
167 
168         mSwitchToTabIconId = iconId;
169         TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(mUrl, mSwitchToTabIconId, 0, 0, 0);
170     }
171 
updateStatusIcon(boolean isBookmark, boolean isReaderItem)172     private void updateStatusIcon(boolean isBookmark, boolean isReaderItem) {
173         if (isReaderItem) {
174             mStatusIcon.setImageResource(R.drawable.status_icon_readercache);
175         } else if (isBookmark) {
176             mStatusIcon.setImageResource(R.drawable.star_blue);
177         }
178 
179         if (mShowIcons && (isBookmark || isReaderItem)) {
180             mStatusIcon.setVisibility(View.VISIBLE);
181         } else if (mShowIcons) {
182             // We use INVISIBLE to have consistent padding for our items. This means text/URLs
183             // fade consistently in the same location, regardless of them being bookmarked.
184             mStatusIcon.setVisibility(View.INVISIBLE);
185         } else {
186             mStatusIcon.setVisibility(View.GONE);
187         }
188 
189     }
190 
191     /**
192      * Stores the page URL, so that we can use it to replace "Switch to tab" if the open
193      * tab changes or is closed.
194      */
updateDisplayedUrl(String url, boolean hasReaderCacheItem)195     private void updateDisplayedUrl(String url, boolean hasReaderCacheItem) {
196         mPageUrl = url;
197         mHasReaderCacheItem = hasReaderCacheItem;
198         updateDisplayedUrl();
199     }
200 
201     /**
202      * Replaces the page URL with "Switch to tab" if there is already a tab open with that URL.
203      * Only looks for tabs that are either private or non-private, depending on the current
204      * selected tab.
205      */
updateDisplayedUrl()206     protected void updateDisplayedUrl() {
207         final Tab selectedTab = Tabs.getInstance().getSelectedTab();
208         final boolean isPrivate = (selectedTab != null) && (selectedTab.isPrivate());
209 
210         // We always want to display the underlying page url, however for readermode pages
211         // we navigate to the about:reader equivalent, hence we need to use that url when finding
212         // existing tabs
213         final String navigationUrl = mHasReaderCacheItem ? ReaderModeUtils.getAboutReaderForUrl(mPageUrl) : mPageUrl;
214         Tab tab = Tabs.getInstance().getFirstTabForUrl(navigationUrl, isPrivate);
215 
216 
217         if (!mShowIcons || tab == null) {
218             setUrl(mPageUrl);
219             setSwitchToTabIcon(NO_ICON);
220         } else {
221             setUrl(R.string.switch_to_tab);
222             setSwitchToTabIcon(R.drawable.ic_url_bar_tab);
223         }
224     }
225 
setShowIcons(boolean showIcons)226     public void setShowIcons(boolean showIcons) {
227         mShowIcons = showIcons;
228     }
229 
230     /**
231      * Update the data displayed by this row.
232      * <p>
233      * This method must be invoked on the UI thread.
234      *
235      * @param title to display.
236      * @param url to display.
237      */
update(String title, String url)238     public void update(String title, String url) {
239         update(title, url, 0, false);
240     }
241 
update(String title, String url, long bookmarkId, boolean hasReaderCacheItem)242     protected void update(String title, String url, long bookmarkId, boolean hasReaderCacheItem) {
243         if (mShowIcons) {
244             // The bookmark id will be 0 (null in database) when the url
245             // is not a bookmark and negative for 'fake' bookmarks.
246             final boolean isBookmark = bookmarkId > 0;
247 
248             updateStatusIcon(isBookmark, hasReaderCacheItem);
249         } else {
250             updateStatusIcon(false, false);
251         }
252 
253         // Use the URL instead of an empty title for consistency with the normal URL
254         // bar view - this is the equivalent of getDisplayTitle() in Tab.java
255         final String titleToShow = TextUtils.isEmpty(title) ? url : title;
256         if (mTitleFormatter != null) {
257             setTitle(mTitleFormatter.format(titleToShow));
258         } else {
259             setTitle(titleToShow);
260         }
261 
262         // No point updating the below things if URL has not changed. Prevents evil Favicon flicker.
263         if (url.equals(mPageUrl)) {
264             return;
265         }
266 
267         // Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB.
268         mFavicon.clearImage();
269 
270         if (mOngoingIconLoad != null) {
271             mOngoingIconLoad.cancel(true);
272         }
273 
274         // Displayed RecentTabsPanel URLs may refer to pages opened in reader mode, so we
275         // remove the about:reader prefix to ensure the Favicon loads properly.
276         final String pageURL = ReaderModeUtils.stripAboutReaderUrl(url);
277 
278         if (TextUtils.isEmpty(pageURL)) {
279             // If url is empty, display the item as-is but do not load an icon if we do not have a page URL (bug 1310622)
280         } else if (bookmarkId < BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
281             mOngoingIconLoad = Icons.with(getContext())
282                     .pageUrl(pageURL)
283                     .skipNetwork()
284                     .privileged(true)
285                     .icon(IconDescriptor.createGenericIcon(
286                             PartnerBookmarksProviderProxy.getUriForIcon(getContext(), bookmarkId).toString()))
287                     .build()
288                     .execute(mFavicon.createIconCallback());
289         } else {
290             mOngoingIconLoad = Icons.with(getContext())
291                     .pageUrl(pageURL)
292                     .skipNetwork()
293                     .build()
294                     .execute(mFavicon.createIconCallback());
295 
296         }
297 
298         updateDisplayedUrl(url, hasReaderCacheItem);
299     }
300 
301     @Override
setPrivateMode(boolean isPrivate)302     public void setPrivateMode(boolean isPrivate) {
303         super.setPrivateMode(isPrivate);
304 
305         mTitle.setPrivateMode(isPrivate);
306         mUrl.setPrivateMode(isPrivate);
307     }
308 
309     /**
310      * Update the data displayed by this row.
311      * <p>
312      * This method must be invoked on the UI thread.
313      *
314      * @param cursor to extract data from.
315      */
updateFromCursor(Cursor cursor)316     public void updateFromCursor(Cursor cursor) {
317         if (cursor == null) {
318             return;
319         }
320 
321         int titleIndex = cursor.getColumnIndexOrThrow(URLColumns.TITLE);
322         final String title = cursor.getString(titleIndex);
323 
324         int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL);
325         final String url = cursor.getString(urlIndex);
326 
327         final long bookmarkId;
328         final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID);
329         if (bookmarkIdIndex != -1) {
330             bookmarkId = cursor.getLong(bookmarkIdIndex);
331         } else {
332             bookmarkId = 0;
333         }
334 
335         SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(getContext());
336         final boolean hasReaderCacheItem = rch.isURLCached(url);
337 
338         update(title, url, bookmarkId, hasReaderCacheItem);
339     }
340 
setTitleFormatter(TitleFormatter formatter)341     public void setTitleFormatter(TitleFormatter formatter) {
342         mTitleFormatter = formatter;
343     }
344 
345     // Use this interface to decorate content in title view.
346     interface TitleFormatter {
format(@onNull CharSequence title)347         CharSequence format(@NonNull CharSequence title);
348     }
349 }
350