|
34 | 34 | import android.database.DatabaseUtils; |
35 | 35 | import android.graphics.Rect; |
36 | 36 | import android.net.Uri; |
| 37 | +import android.os.Bundle; |
37 | 38 | import android.os.RemoteException; |
38 | 39 | import android.text.TextUtils; |
39 | 40 | import android.util.DisplayMetrics; |
|
44 | 45 | import java.io.IOException; |
45 | 46 | import java.io.InputStream; |
46 | 47 | import java.util.ArrayList; |
| 48 | +import java.util.List; |
| 49 | +import java.util.regex.Matcher; |
| 50 | +import java.util.regex.Pattern; |
47 | 51 |
|
48 | 52 | /** |
49 | 53 | * <p> |
@@ -166,6 +170,22 @@ public final class ContactsContract { |
166 | 170 | */ |
167 | 171 | public static final String STREQUENT_PHONE_ONLY = "strequent_phone_only"; |
168 | 172 |
|
| 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 | + |
169 | 189 | /** |
170 | 190 | * @hide |
171 | 191 | */ |
@@ -4857,6 +4877,19 @@ public static class SearchSnippetColumns { |
4857 | 4877 | * @hide |
4858 | 4878 | */ |
4859 | 4879 | 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"; |
4860 | 4893 | } |
4861 | 4894 |
|
4862 | 4895 | /** |
@@ -8054,4 +8087,138 @@ public static final class Insert { |
8054 | 8087 | public static final String DATA_SET = "com.android.contacts.extra.DATA_SET"; |
8055 | 8088 | } |
8056 | 8089 | } |
| 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 | + |
8057 | 8224 | } |
0 commit comments