Skip to content

Commit 5e87525

Browse files
Isaac KatzenelsonAndroid (Google) Code Review
authored andcommitted
Merge "Fix snippetizing cursor"
2 parents 1356160 + 9fe83f0 commit 5e87525

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed

core/java/android/provider/ContactsContract.java

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import android.database.DatabaseUtils;
3535
import android.graphics.Rect;
3636
import android.net.Uri;
37+
import android.os.Bundle;
3738
import android.os.RemoteException;
3839
import android.text.TextUtils;
3940
import android.util.DisplayMetrics;
@@ -44,6 +45,9 @@
4445
import java.io.IOException;
4546
import java.io.InputStream;
4647
import java.util.ArrayList;
48+
import java.util.List;
49+
import java.util.regex.Matcher;
50+
import java.util.regex.Pattern;
4751

4852
/**
4953
* <p>
@@ -166,6 +170,22 @@ public final class ContactsContract {
166170
*/
167171
public static final String STREQUENT_PHONE_ONLY = "strequent_phone_only";
168172

173+
/**
174+
* A key to a boolean in the "extras" bundle of the cursor.
175+
* The boolean indicates that the provider did not create a snippet and that the client asking
176+
* for the snippet should do it (true means the snippeting was deferred to the client).
177+
*
178+
* @hide
179+
*/
180+
public static final String DEFERRED_SNIPPETING = "deferred_snippeting";
181+
182+
/**
183+
* Key to retrieve the original query on the client side.
184+
*
185+
* @hide
186+
*/
187+
public static final String DEFERRED_SNIPPETING_QUERY = "deferred_snippeting_query";
188+
169189
/**
170190
* @hide
171191
*/
@@ -4857,6 +4877,19 @@ public static class SearchSnippetColumns {
48574877
* @hide
48584878
*/
48594879
public static final String SNIPPET_ARGS_PARAM_KEY = "snippet_args";
4880+
4881+
/**
4882+
* A key to ask the provider to defer the snippeting to the client if possible.
4883+
* Value of 1 implies true, 0 implies false when 0 is the default.
4884+
* When a cursor is returned to the client, it should check for an extra with the name
4885+
* {@link ContactsContract#DEFERRED_SNIPPETING} in the cursor. If it exists, the client
4886+
* should do its own snippeting using {@link ContactsContract#snippetize}. If
4887+
* it doesn't exist, the snippet column in the cursor should already contain a snippetized
4888+
* string.
4889+
*
4890+
* @hide
4891+
*/
4892+
public static final String DEFERRED_SNIPPETING_KEY = "deferred_snippeting";
48604893
}
48614894

48624895
/**
@@ -8054,4 +8087,138 @@ public static final class Insert {
80548087
public static final String DATA_SET = "com.android.contacts.extra.DATA_SET";
80558088
}
80568089
}
8090+
8091+
/**
8092+
* Creates a snippet out of the given content that matches the given query.
8093+
* @param content - The content to use to compute the snippet.
8094+
* @param displayName - Display name for the contact - if this already contains the search
8095+
* content, no snippet should be shown.
8096+
* @param query - String to search for in the content.
8097+
* @param snippetStartMatch - Marks the start of the matching string in the snippet.
8098+
* @param snippetEndMatch - Marks the end of the matching string in the snippet.
8099+
* @param snippetEllipsis - Ellipsis string appended to the end of the snippet (if too long).
8100+
* @param snippetMaxTokens - Maximum number of words from the snippet that will be displayed.
8101+
* @return The computed snippet, or null if the snippet could not be computed or should not be
8102+
* shown.
8103+
*
8104+
* @hide
8105+
*/
8106+
public static String snippetize(String content, String displayName, String query,
8107+
char snippetStartMatch, char snippetEndMatch, String snippetEllipsis,
8108+
int snippetMaxTokens) {
8109+
8110+
String lowerQuery = query != null ? query.toLowerCase() : null;
8111+
if (TextUtils.isEmpty(content) || TextUtils.isEmpty(query) ||
8112+
TextUtils.isEmpty(displayName) || !content.toLowerCase().contains(lowerQuery)) {
8113+
return null;
8114+
}
8115+
8116+
// If the display name already contains the query term, return empty - snippets should
8117+
// not be needed in that case.
8118+
String lowerDisplayName = displayName != null ? displayName.toLowerCase() : "";
8119+
List<String> nameTokens = new ArrayList<String>();
8120+
List<Integer> nameTokenOffsets = new ArrayList<Integer>();
8121+
split(lowerDisplayName.trim(), nameTokens, nameTokenOffsets);
8122+
for (String nameToken : nameTokens) {
8123+
if (nameToken.startsWith(lowerQuery)) {
8124+
return null;
8125+
}
8126+
}
8127+
8128+
String[] contentLines = content.split("\n");
8129+
8130+
// Locate the lines of the content that contain the query term.
8131+
for (String contentLine : contentLines) {
8132+
if (contentLine.toLowerCase().contains(lowerQuery)) {
8133+
8134+
// Line contains the query string - now search for it at the start of tokens.
8135+
List<String> lineTokens = new ArrayList<String>();
8136+
List<Integer> tokenOffsets = new ArrayList<Integer>();
8137+
split(contentLine.trim(), lineTokens, tokenOffsets);
8138+
8139+
// As we find matches against the query, we'll populate this list with the marked
8140+
// (or unchanged) tokens.
8141+
List<String> markedTokens = new ArrayList<String>();
8142+
8143+
int firstToken = -1;
8144+
int lastToken = -1;
8145+
for (int i = 0; i < lineTokens.size(); i++) {
8146+
String token = lineTokens.get(i);
8147+
String lowerToken = token.toLowerCase();
8148+
if (lowerToken.startsWith(lowerQuery)) {
8149+
8150+
// Query term matched; surround the token with match markers.
8151+
markedTokens.add(snippetStartMatch + token + snippetEndMatch);
8152+
8153+
// If this is the first token found with a match, mark the token
8154+
// positions to use for assembling the snippet.
8155+
if (firstToken == -1) {
8156+
firstToken =
8157+
Math.max(0, i - (int) Math.floor(
8158+
Math.abs(snippetMaxTokens)
8159+
/ 2.0));
8160+
lastToken =
8161+
Math.min(lineTokens.size(), firstToken +
8162+
Math.abs(snippetMaxTokens));
8163+
}
8164+
} else {
8165+
markedTokens.add(token);
8166+
}
8167+
}
8168+
8169+
// Assemble the snippet by piecing the tokens back together.
8170+
if (firstToken > -1) {
8171+
StringBuilder sb = new StringBuilder();
8172+
if (firstToken > 0) {
8173+
sb.append(snippetEllipsis);
8174+
}
8175+
for (int i = firstToken; i < lastToken; i++) {
8176+
String markedToken = markedTokens.get(i);
8177+
String originalToken = lineTokens.get(i);
8178+
sb.append(markedToken);
8179+
if (i < lastToken - 1) {
8180+
// Add the characters that appeared between this token and the next.
8181+
sb.append(contentLine.substring(
8182+
tokenOffsets.get(i) + originalToken.length(),
8183+
tokenOffsets.get(i + 1)));
8184+
}
8185+
}
8186+
if (lastToken < lineTokens.size()) {
8187+
sb.append(snippetEllipsis);
8188+
}
8189+
return sb.toString();
8190+
}
8191+
}
8192+
}
8193+
return null;
8194+
}
8195+
8196+
/**
8197+
* Pattern for splitting a line into tokens. This matches e-mail addresses as a single token,
8198+
* otherwise splitting on any group of non-alphanumeric characters.
8199+
*
8200+
* @hide
8201+
*/
8202+
private static Pattern SPLIT_PATTERN =
8203+
Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
8204+
8205+
/**
8206+
* Helper method for splitting a string into tokens. The lists passed in are populated with the
8207+
* tokens and offsets into the content of each token. The tokenization function parses e-mail
8208+
* addresses as a single token; otherwise it splits on any non-alphanumeric character.
8209+
* @param content Content to split.
8210+
* @param tokens List of token strings to populate.
8211+
* @param offsets List of offsets into the content for each token returned.
8212+
*
8213+
* @hide
8214+
*/
8215+
private static void split(String content, List<String> tokens, List<Integer> offsets) {
8216+
Matcher matcher = SPLIT_PATTERN.matcher(content);
8217+
while (matcher.find()) {
8218+
tokens.add(matcher.group());
8219+
offsets.add(matcher.start());
8220+
}
8221+
}
8222+
8223+
80578224
}

0 commit comments

Comments
 (0)