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