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