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.activitystream.homepanel.model; 7 8 import android.database.Cursor; 9 import android.support.annotation.NonNull; 10 import android.support.annotation.Nullable; 11 import android.support.annotation.VisibleForTesting; 12 import android.text.TextUtils; 13 import org.mozilla.gecko.activitystream.Utils; 14 import org.mozilla.gecko.activitystream.homepanel.StreamRecyclerAdapter; 15 import org.mozilla.gecko.activitystream.ranking.HighlightCandidateCursorIndices; 16 import org.mozilla.gecko.activitystream.ranking.HighlightsRanking; 17 18 import java.util.regex.Matcher; 19 import java.util.regex.Pattern; 20 21 public class Highlight implements WebpageRowModel { 22 23 /** 24 * A pattern matching a json object containing the key "image_url" and extracting the value. afaik, these urls 25 * are not encoded so it's entirely possible that the url will contain a quote and we will not extract the whole 26 * url. However, given these are coming from websites providing favicon-like images, it's not likely a quote will 27 * appear and since these urls are only being used to compare against one another (as imageURLs in Highlight items), 28 * a partial URL may actually have the same behavior: good enough for me! 29 */ 30 private static final Pattern FAST_IMAGE_URL_PATTERN = Pattern.compile("\"image_url\":\"([^\"]+)\""); 31 32 // A pattern matching a json object containing the key "description_length" and extracting the value: this 33 // regex should perfectly match values in json without whitespace. 34 private static final Pattern FAST_DESCRIPTION_LENGTH_PATTERN = Pattern.compile("\"description_length\":([0-9]+)"); 35 36 private final String title; 37 private final String url; 38 private final Utils.HighlightSource source; 39 40 private long historyId; 41 42 private @Nullable Metadata metadata; // lazily-loaded. 43 private @Nullable final String metadataJSON; 44 private @Nullable String fastImageURL; 45 private int fastDescriptionLength; 46 47 private @Nullable Boolean isPinned; 48 private @Nullable Boolean isBookmarked; 49 fromCursor(final Cursor cursor, final HighlightCandidateCursorIndices cursorIndices)50 public static Highlight fromCursor(final Cursor cursor, final HighlightCandidateCursorIndices cursorIndices) { 51 return new Highlight(cursor, cursorIndices); 52 } 53 Highlight(final Cursor cursor, final HighlightCandidateCursorIndices cursorIndices)54 private Highlight(final Cursor cursor, final HighlightCandidateCursorIndices cursorIndices) { 55 title = cursor.getString(cursorIndices.titleColumnIndex); 56 url = cursor.getString(cursorIndices.urlColumnIndex); 57 source = Utils.highlightSource(cursor, cursorIndices); 58 59 historyId = cursor.getLong(cursorIndices.historyIDColumnIndex); 60 61 metadataJSON = cursor.getString(cursorIndices.metadataColumnIndex); 62 fastImageURL = initFastImageURL(metadataJSON); 63 fastDescriptionLength = initFastDescriptionLength(metadataJSON); 64 65 updateState(); 66 } 67 68 /** Gets a fast image URL. Full docs for this method at {@link #getFastImageURLForComparison()} & {@link #FAST_IMAGE_URL_PATTERN}. */ initFastImageURL(final String metadataJSON)69 @VisibleForTesting static @Nullable String initFastImageURL(final String metadataJSON) { 70 return extractFirstGroupFromMetadataJSON(metadataJSON, FAST_IMAGE_URL_PATTERN); 71 } 72 73 /** Gets a fast description length. Full docs for this method at {@link #getFastDescriptionLength()} & {@link #FAST_DESCRIPTION_LENGTH_PATTERN}. */ initFastDescriptionLength(final String metadataJSON)74 @VisibleForTesting static int initFastDescriptionLength(final String metadataJSON) { 75 final String extractedStr = extractFirstGroupFromMetadataJSON(metadataJSON, FAST_DESCRIPTION_LENGTH_PATTERN); 76 try { 77 return !TextUtils.isEmpty(extractedStr) ? Integer.parseInt(extractedStr) : 0; 78 } catch (final NumberFormatException e) { /* intentionally blank */ } 79 return 0; 80 } 81 extractFirstGroupFromMetadataJSON(final String metadataJSON, final Pattern pattern)82 private static @Nullable String extractFirstGroupFromMetadataJSON(final String metadataJSON, final Pattern pattern) { 83 if (metadataJSON == null) { 84 return null; 85 } 86 87 final Matcher matcher = pattern.matcher(metadataJSON); 88 return matcher.find() ? matcher.group(1) : null; 89 } 90 91 @Override getRowItemType()92 public StreamRecyclerAdapter.RowItemType getRowItemType() { 93 return StreamRecyclerAdapter.RowItemType.HIGHLIGHT_ITEM; 94 } 95 updateState()96 private void updateState() { 97 // We can only be certain of bookmark state if an item is a bookmark item. 98 // Otherwise, due to the underlying highlights query, we have to look up states when 99 // menus are displayed. 100 switch (source) { 101 case BOOKMARKED: 102 isBookmarked = true; 103 isPinned = null; 104 break; 105 case VISITED: 106 isBookmarked = null; 107 isPinned = null; 108 break; 109 default: 110 throw new IllegalArgumentException("Unknown source: " + source); 111 } 112 } 113 getTitle()114 public String getTitle() { 115 return title; 116 } 117 getUrl()118 public String getUrl() { 119 return url; 120 } 121 122 /** 123 * Retrieves the metadata associated with this highlight, lazily loaded. 124 * 125 * AVOID USING THIS FOR A LARGE NUMBER OF ITEMS, particularly in {@link HighlightsRanking#extractFeatures(Cursor)}, 126 * where we added lazy loading to improve performance. 127 * 128 * The JSONObject constructor inside Metadata takes a non-trivial amount of time to run so 129 * we lazily load it. At the time of writing, in {@link HighlightsRanking#extractFeatures(Cursor)}, we get 130 * 500 highlights before curating down to the ~5 shown items. For the non-displayed items, we use 131 * the getFast* methods and, for the shown items, lazy-load the metadata since only then is it necessary. 132 * These methods include: 133 * - {@link #getFastDescriptionLength()} 134 * - {@link #getFastImageURLForComparison()} 135 * - {@link #hasFastImageURL()} 136 */ 137 @NonNull getMetadataSlow()138 public Metadata getMetadataSlow() { 139 if (metadata == null) { 140 metadata = new Metadata(metadataJSON); 141 } 142 return metadata; 143 } 144 145 /** 146 * Returns the image URL associated with this Highlight. 147 * 148 * This implementation may be slow: see {@link #getMetadataSlow()}. 149 * 150 * @return the image URL, or the empty String if there is none. 151 */ 152 @NonNull 153 @Override getImageUrl()154 public String getImageUrl() { 155 final Metadata metadata = getMetadataSlow(); 156 final String imageUrl = metadata.getImageUrl(); 157 return imageUrl != null ? imageUrl : ""; 158 } 159 160 /** 161 * Returns the image url in the highlight's metadata. This value does not provide valid image url but is 162 * consistent across invocations and can be used to compare against other Highlight's fast image urls. 163 * See {@link #getMetadataSlow()} for a description of why we use this method. 164 * 165 * To get a valid image url (at a performance penalty), use {@link #getMetadataSlow()} 166 * {@link #getMetadataSlow()} & {@link Metadata#getImageUrl()}. 167 * 168 * Note that this explanation is dependent on the implementation of {@link #initFastImageURL(String)}. 169 * 170 * @return the image url, or null if one could not be found. 171 */ getFastImageURLForComparison()172 public @Nullable String getFastImageURLForComparison() { 173 return fastImageURL; 174 } 175 176 /** 177 * Returns true if {@link #getFastImageURLForComparison()} has found an image url, false otherwise. 178 * See that method for caveats. 179 */ hasFastImageURL()180 public boolean hasFastImageURL() { 181 return fastImageURL != null; 182 } 183 184 /** 185 * Returns the description length in the highlight's metadata. This value is expected to correct in all cases. 186 * See {@link #getMetadataSlow()} for why we use this method. 187 * 188 * This is a faster version of {@link #getMetadataSlow()} & {@link Metadata#getDescriptionLength()} because 189 * retrieving the metadata in this way does a full json parse, which is slower. 190 * 191 * Note: this explanation is dependent on the implementation of {@link #initFastDescriptionLength(String)}. 192 * 193 * @return the given description length, or 0 if no description length was given 194 */ getFastDescriptionLength()195 public int getFastDescriptionLength() { 196 return fastDescriptionLength; 197 } 198 isBookmarked()199 public Boolean isBookmarked() { 200 return isBookmarked; 201 } 202 isPinned()203 public Boolean isPinned() { 204 return isPinned; 205 } 206 207 @Override updateBookmarked(boolean bookmarked)208 public void updateBookmarked(boolean bookmarked) { 209 this.isBookmarked = bookmarked; 210 } 211 212 @Override updatePinned(boolean pinned)213 public void updatePinned(boolean pinned) { 214 this.isPinned = pinned; 215 } 216 217 @Override getSource()218 public Utils.HighlightSource getSource() { 219 return source; 220 } 221 222 @Override getUniqueId()223 public long getUniqueId() { 224 return historyId; 225 } 226 227 // The Highlights cursor automatically notifies of data changes, so nothing needs to be done here. 228 @Override onStateCommitted()229 public void onStateCommitted() {} 230 } 231