Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2017 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package android.view.textclassifier; |
| 18 | |
| 19 | import android.annotation.NonNull; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 20 | import android.annotation.Nullable; |
Abodunrinwa Toki | fafdb73 | 2017-02-02 11:07:05 +0000 | [diff] [blame] | 21 | import android.content.ComponentName; |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 22 | import android.content.Context; |
| 23 | import android.content.Intent; |
| 24 | import android.content.pm.PackageManager; |
| 25 | import android.content.pm.ResolveInfo; |
| 26 | import android.graphics.drawable.Drawable; |
| 27 | import android.net.Uri; |
Abodunrinwa Toki | 4cfda0b | 2017-02-28 18:56:47 +0000 | [diff] [blame] | 28 | import android.os.LocaleList; |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 29 | import android.os.ParcelFileDescriptor; |
Abodunrinwa Toki | 9b4c82a | 2017-02-06 20:29:36 +0000 | [diff] [blame] | 30 | import android.provider.Browser; |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 31 | import android.provider.ContactsContract; |
Abodunrinwa Toki | 0e6b43e | 2017-09-19 23:18:40 +0100 | [diff] [blame] | 32 | import android.provider.Settings; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 33 | import android.text.util.Linkify; |
Abodunrinwa Toki | d2d1399 | 2017-03-24 21:43:13 +0000 | [diff] [blame] | 34 | import android.util.Patterns; |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 35 | |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 36 | import com.android.internal.annotations.GuardedBy; |
Abodunrinwa Toki | 1d77557 | 2017-05-08 16:03:01 +0100 | [diff] [blame] | 37 | import com.android.internal.logging.MetricsLogger; |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 38 | import com.android.internal.util.Preconditions; |
| 39 | |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 40 | import java.io.File; |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 41 | import java.io.FileNotFoundException; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 42 | import java.io.IOException; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 43 | import java.util.ArrayList; |
Richard Ledley | db18a57 | 2017-11-30 17:33:51 +0000 | [diff] [blame] | 44 | import java.util.Arrays; |
| 45 | import java.util.Collection; |
| 46 | import java.util.Collections; |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 47 | import java.util.HashMap; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 48 | import java.util.List; |
Abodunrinwa Toki | 9b4c82a | 2017-02-06 20:29:36 +0000 | [diff] [blame] | 49 | import java.util.Locale; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 50 | import java.util.Map; |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 51 | import java.util.Objects; |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 52 | import java.util.regex.Matcher; |
| 53 | import java.util.regex.Pattern; |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 54 | |
| 55 | /** |
| 56 | * Default implementation of the {@link TextClassifier} interface. |
| 57 | * |
| 58 | * <p>This class uses machine learning to recognize entities in text. |
| 59 | * Unless otherwise stated, methods of this class are blocking operations and should most |
| 60 | * likely not be called on the UI thread. |
| 61 | * |
| 62 | * @hide |
| 63 | */ |
| 64 | final class TextClassifierImpl implements TextClassifier { |
| 65 | |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 66 | private static final String LOG_TAG = DEFAULT_LOG_TAG; |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 67 | private static final String MODEL_DIR = "/etc/textclassifier/"; |
| 68 | private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model"; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 69 | private static final String UPDATED_MODEL_FILE_PATH = |
| 70 | "/data/misc/textclassifier/textclassifier.smartselection.model"; |
Richard Ledley | db18a57 | 2017-11-30 17:33:51 +0000 | [diff] [blame] | 71 | private static final List<String> ENTITY_TYPES_ALL = |
| 72 | Collections.unmodifiableList(Arrays.asList( |
| 73 | TextClassifier.TYPE_ADDRESS, |
| 74 | TextClassifier.TYPE_EMAIL, |
| 75 | TextClassifier.TYPE_PHONE, |
| 76 | TextClassifier.TYPE_URL)); |
| 77 | private static final List<String> ENTITY_TYPES_BASE = |
| 78 | Collections.unmodifiableList(Arrays.asList( |
| 79 | TextClassifier.TYPE_ADDRESS, |
| 80 | TextClassifier.TYPE_EMAIL, |
| 81 | TextClassifier.TYPE_PHONE, |
| 82 | TextClassifier.TYPE_URL)); |
Abodunrinwa Toki | b89cf02 | 2017-02-06 19:53:22 +0000 | [diff] [blame] | 83 | |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 84 | private final Context mContext; |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 85 | |
Abodunrinwa Toki | 1d77557 | 2017-05-08 16:03:01 +0100 | [diff] [blame] | 86 | private final MetricsLogger mMetricsLogger = new MetricsLogger(); |
| 87 | |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 88 | private final Object mSmartSelectionLock = new Object(); |
| 89 | @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. |
| 90 | private Map<Locale, String> mModelFilePaths; |
| 91 | @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. |
| 92 | private Locale mLocale; |
| 93 | @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 94 | private int mVersion; |
| 95 | @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 96 | private SmartSelection mSmartSelection; |
| 97 | |
Abodunrinwa Toki | 0e6b43e | 2017-09-19 23:18:40 +0100 | [diff] [blame] | 98 | private TextClassifierConstants mSettings; |
| 99 | |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 100 | TextClassifierImpl(Context context) { |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 101 | mContext = Preconditions.checkNotNull(context); |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 102 | } |
| 103 | |
| 104 | @Override |
| 105 | public TextSelection suggestSelection( |
Abodunrinwa Toki | 4cfda0b | 2017-02-28 18:56:47 +0000 | [diff] [blame] | 106 | @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex, |
Abodunrinwa Toki | 4d232d6 | 2017-11-23 12:22:45 +0000 | [diff] [blame] | 107 | @NonNull TextSelection.Options options) { |
| 108 | Utils.validateInput(text, selectionStartIndex, selectionEndIndex); |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 109 | try { |
| 110 | if (text.length() > 0) { |
Abodunrinwa Toki | 2b6020f | 2017-10-28 02:28:45 +0100 | [diff] [blame] | 111 | final LocaleList locales = (options == null) ? null : options.getDefaultLocales(); |
| 112 | final SmartSelection smartSelection = getSmartSelection(locales); |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 113 | final String string = text.toString(); |
Abodunrinwa Toki | 2b6020f | 2017-10-28 02:28:45 +0100 | [diff] [blame] | 114 | final int start; |
| 115 | final int end; |
| 116 | if (getSettings().isDarkLaunch() && !options.isDarkLaunchAllowed()) { |
| 117 | start = selectionStartIndex; |
| 118 | end = selectionEndIndex; |
| 119 | } else { |
| 120 | final int[] startEnd = smartSelection.suggest( |
| 121 | string, selectionStartIndex, selectionEndIndex); |
| 122 | start = startEnd[0]; |
| 123 | end = startEnd[1]; |
| 124 | } |
Abodunrinwa Toki | b416297 | 2017-05-05 18:07:17 +0100 | [diff] [blame] | 125 | if (start <= end |
| 126 | && start >= 0 && end <= string.length() |
| 127 | && start <= selectionStartIndex && end >= selectionEndIndex) { |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 128 | final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end); |
Abodunrinwa Toki | a6096f6 | 2017-03-08 17:21:40 +0000 | [diff] [blame] | 129 | final SmartSelection.ClassificationResult[] results = |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 130 | smartSelection.classifyText( |
Abodunrinwa Toki | d2d1399 | 2017-03-24 21:43:13 +0000 | [diff] [blame] | 131 | string, start, end, |
| 132 | getHintFlags(string, start, end)); |
Abodunrinwa Toki | a6096f6 | 2017-03-08 17:21:40 +0000 | [diff] [blame] | 133 | final int size = results.length; |
| 134 | for (int i = 0; i < size; i++) { |
| 135 | tsBuilder.setEntityType(results[i].mCollection, results[i].mScore); |
| 136 | } |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 137 | return tsBuilder |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 138 | .setSignature( |
| 139 | getSignature(string, selectionStartIndex, selectionEndIndex)) |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 140 | .build(); |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 141 | } else { |
| 142 | // We can not trust the result. Log the issue and ignore the result. |
| 143 | Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result."); |
| 144 | } |
| 145 | } |
| 146 | } catch (Throwable t) { |
| 147 | // Avoid throwing from this method. Log the error. |
| 148 | Log.e(LOG_TAG, |
| 149 | "Error suggesting selection for text. No changes to selection suggested.", |
| 150 | t); |
| 151 | } |
| 152 | // Getting here means something went wrong, return a NO_OP result. |
| 153 | return TextClassifier.NO_OP.suggestSelection( |
Abodunrinwa Toki | 2b6020f | 2017-10-28 02:28:45 +0100 | [diff] [blame] | 154 | text, selectionStartIndex, selectionEndIndex, options); |
| 155 | } |
| 156 | |
| 157 | @Override |
Abodunrinwa Toki | e0b5789 | 2017-04-28 19:59:57 +0100 | [diff] [blame] | 158 | public TextClassification classifyText( |
Abodunrinwa Toki | a2df6e5 | 2017-04-13 09:56:52 +0100 | [diff] [blame] | 159 | @NonNull CharSequence text, int startIndex, int endIndex, |
Abodunrinwa Toki | 4d232d6 | 2017-11-23 12:22:45 +0000 | [diff] [blame] | 160 | @NonNull TextClassification.Options options) { |
| 161 | Utils.validateInput(text, startIndex, endIndex); |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 162 | try { |
| 163 | if (text.length() > 0) { |
Abodunrinwa Toki | d2d1399 | 2017-03-24 21:43:13 +0000 | [diff] [blame] | 164 | final String string = text.toString(); |
Abodunrinwa Toki | 2b6020f | 2017-10-28 02:28:45 +0100 | [diff] [blame] | 165 | final LocaleList locales = (options == null) ? null : options.getDefaultLocales(); |
| 166 | final SmartSelection.ClassificationResult[] results = getSmartSelection(locales) |
Abodunrinwa Toki | d2d1399 | 2017-03-24 21:43:13 +0000 | [diff] [blame] | 167 | .classifyText(string, startIndex, endIndex, |
| 168 | getHintFlags(string, startIndex, endIndex)); |
Abodunrinwa Toki | a6096f6 | 2017-03-08 17:21:40 +0000 | [diff] [blame] | 169 | if (results.length > 0) { |
Abodunrinwa Toki | e0b5789 | 2017-04-28 19:59:57 +0100 | [diff] [blame] | 170 | final TextClassification classificationResult = |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 171 | createClassificationResult(results, string, startIndex, endIndex); |
Abodunrinwa Toki | 9841ea0 | 2017-03-22 20:11:35 +0000 | [diff] [blame] | 172 | return classificationResult; |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 173 | } |
| 174 | } |
| 175 | } catch (Throwable t) { |
| 176 | // Avoid throwing from this method. Log the error. |
Abodunrinwa Toki | 7861885 | 2017-10-17 15:31:39 +0100 | [diff] [blame] | 177 | Log.e(LOG_TAG, "Error getting text classification info.", t); |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 178 | } |
| 179 | // Getting here means something went wrong, return a NO_OP result. |
Abodunrinwa Toki | 2b6020f | 2017-10-28 02:28:45 +0100 | [diff] [blame] | 180 | return TextClassifier.NO_OP.classifyText(text, startIndex, endIndex, options); |
| 181 | } |
| 182 | |
| 183 | @Override |
Richard Ledley | 68d9452 | 2017-10-05 10:52:19 +0100 | [diff] [blame] | 184 | public TextLinks generateLinks( |
Richard Ledley | db18a57 | 2017-11-30 17:33:51 +0000 | [diff] [blame] | 185 | @NonNull CharSequence text, @Nullable TextLinks.Options options) { |
Abodunrinwa Toki | 4d232d6 | 2017-11-23 12:22:45 +0000 | [diff] [blame] | 186 | Utils.validateInput(text); |
Richard Ledley | 68d9452 | 2017-10-05 10:52:19 +0100 | [diff] [blame] | 187 | final String textString = text.toString(); |
| 188 | final TextLinks.Builder builder = new TextLinks.Builder(textString); |
Richard Ledley | 9cfa606 | 2018-01-15 13:13:29 +0000 | [diff] [blame] | 189 | |
| 190 | if (!getSettings().isSmartLinkifyEnabled()) { |
| 191 | return builder.build(); |
| 192 | } |
| 193 | |
Richard Ledley | 68d9452 | 2017-10-05 10:52:19 +0100 | [diff] [blame] | 194 | try { |
Richard Ledley | db18a57 | 2017-11-30 17:33:51 +0000 | [diff] [blame] | 195 | final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null; |
| 196 | final Collection<String> entitiesToIdentify = |
| 197 | options != null && options.getEntityConfig() != null |
| 198 | ? options.getEntityConfig().getEntities(this) : ENTITY_TYPES_ALL; |
Richard Ledley | 68d9452 | 2017-10-05 10:52:19 +0100 | [diff] [blame] | 199 | final SmartSelection smartSelection = getSmartSelection(defaultLocales); |
| 200 | final SmartSelection.AnnotatedSpan[] annotations = smartSelection.annotate(textString); |
| 201 | for (SmartSelection.AnnotatedSpan span : annotations) { |
Richard Ledley | 68d9452 | 2017-10-05 10:52:19 +0100 | [diff] [blame] | 202 | final SmartSelection.ClassificationResult[] results = span.getClassification(); |
Richard Ledley | db18a57 | 2017-11-30 17:33:51 +0000 | [diff] [blame] | 203 | if (results.length == 0 || !entitiesToIdentify.contains(results[0].mCollection)) { |
| 204 | continue; |
| 205 | } |
| 206 | final Map<String, Float> entityScores = new HashMap<>(); |
Richard Ledley | 68d9452 | 2017-10-05 10:52:19 +0100 | [diff] [blame] | 207 | for (int i = 0; i < results.length; i++) { |
| 208 | entityScores.put(results[i].mCollection, results[i].mScore); |
| 209 | } |
| 210 | builder.addLink(new TextLinks.TextLink( |
| 211 | textString, span.getStartIndex(), span.getEndIndex(), entityScores)); |
| 212 | } |
| 213 | } catch (Throwable t) { |
| 214 | // Avoid throwing from this method. Log the error. |
| 215 | Log.e(LOG_TAG, "Error getting links info.", t); |
| 216 | } |
| 217 | return builder.build(); |
| 218 | } |
| 219 | |
| 220 | @Override |
Richard Ledley | db18a57 | 2017-11-30 17:33:51 +0000 | [diff] [blame] | 221 | public Collection<String> getEntitiesForPreset(@TextClassifier.EntityPreset int entityPreset) { |
| 222 | switch (entityPreset) { |
| 223 | case TextClassifier.ENTITY_PRESET_NONE: |
| 224 | return Collections.emptyList(); |
| 225 | case TextClassifier.ENTITY_PRESET_BASE: |
| 226 | return ENTITY_TYPES_BASE; |
| 227 | case TextClassifier.ENTITY_PRESET_ALL: |
| 228 | // fall through |
| 229 | default: |
| 230 | return ENTITY_TYPES_ALL; |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | @Override |
Abodunrinwa Toki | 1d77557 | 2017-05-08 16:03:01 +0100 | [diff] [blame] | 235 | public void logEvent(String source, String event) { |
| 236 | if (LOG_TAG.equals(source)) { |
| 237 | mMetricsLogger.count(event, 1); |
| 238 | } |
| 239 | } |
| 240 | |
Abodunrinwa Toki | 0e6b43e | 2017-09-19 23:18:40 +0100 | [diff] [blame] | 241 | @Override |
| 242 | public TextClassifierConstants getSettings() { |
| 243 | if (mSettings == null) { |
| 244 | mSettings = TextClassifierConstants.loadFromString(Settings.Global.getString( |
| 245 | mContext.getContentResolver(), Settings.Global.TEXT_CLASSIFIER_CONSTANTS)); |
| 246 | } |
| 247 | return mSettings; |
| 248 | } |
| 249 | |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 250 | private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException { |
Abodunrinwa Toki | b89cf02 | 2017-02-06 19:53:22 +0000 | [diff] [blame] | 251 | synchronized (mSmartSelectionLock) { |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 252 | localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList; |
| 253 | final Locale locale = findBestSupportedLocaleLocked(localeList); |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 254 | if (locale == null) { |
| 255 | throw new FileNotFoundException("No file for null locale"); |
| 256 | } |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 257 | if (mSmartSelection == null || !Objects.equals(mLocale, locale)) { |
| 258 | destroySmartSelectionIfExistsLocked(); |
Abodunrinwa Toki | 6ace893 | 2017-04-28 19:25:24 +0100 | [diff] [blame] | 259 | final ParcelFileDescriptor fd = getFdLocked(locale); |
Jan Althaus | e750c6c | 2017-11-13 12:07:04 +0100 | [diff] [blame] | 260 | final int modelFd = fd.getFd(); |
| 261 | mVersion = SmartSelection.getVersion(modelFd); |
| 262 | mSmartSelection = new SmartSelection(modelFd); |
Abodunrinwa Toki | 6ace893 | 2017-04-28 19:25:24 +0100 | [diff] [blame] | 263 | closeAndLogError(fd); |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 264 | mLocale = locale; |
Abodunrinwa Toki | b89cf02 | 2017-02-06 19:53:22 +0000 | [diff] [blame] | 265 | } |
| 266 | return mSmartSelection; |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 267 | } |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 268 | } |
| 269 | |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 270 | private String getSignature(String text, int start, int end) { |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 271 | synchronized (mSmartSelectionLock) { |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 272 | final String versionInfo = (mLocale != null) |
| 273 | ? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion) |
| 274 | : ""; |
| 275 | final int hash = Objects.hash(text, start, end, mContext.getPackageName()); |
| 276 | return String.format(Locale.US, "%s|%s|%d", LOG_TAG, versionInfo, hash); |
Abodunrinwa Toki | 692b196 | 2017-08-15 15:05:11 +0100 | [diff] [blame] | 277 | } |
| 278 | } |
| 279 | |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 280 | @GuardedBy("mSmartSelectionLock") // Do not call outside this lock. |
Abodunrinwa Toki | 6ace893 | 2017-04-28 19:25:24 +0100 | [diff] [blame] | 281 | private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException { |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 282 | ParcelFileDescriptor updateFd; |
Jan Althaus | e750c6c | 2017-11-13 12:07:04 +0100 | [diff] [blame] | 283 | int updateVersion = -1; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 284 | try { |
| 285 | updateFd = ParcelFileDescriptor.open( |
| 286 | new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY); |
Jan Althaus | e750c6c | 2017-11-13 12:07:04 +0100 | [diff] [blame] | 287 | if (updateFd != null) { |
| 288 | updateVersion = SmartSelection.getVersion(updateFd.getFd()); |
| 289 | } |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 290 | } catch (FileNotFoundException e) { |
| 291 | updateFd = null; |
| 292 | } |
| 293 | ParcelFileDescriptor factoryFd; |
Jan Althaus | e750c6c | 2017-11-13 12:07:04 +0100 | [diff] [blame] | 294 | int factoryVersion = -1; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 295 | try { |
| 296 | final String factoryModelFilePath = getFactoryModelFilePathsLocked().get(locale); |
| 297 | if (factoryModelFilePath != null) { |
| 298 | factoryFd = ParcelFileDescriptor.open( |
| 299 | new File(factoryModelFilePath), ParcelFileDescriptor.MODE_READ_ONLY); |
Jan Althaus | e750c6c | 2017-11-13 12:07:04 +0100 | [diff] [blame] | 300 | if (factoryFd != null) { |
| 301 | factoryVersion = SmartSelection.getVersion(factoryFd.getFd()); |
| 302 | } |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 303 | } else { |
| 304 | factoryFd = null; |
| 305 | } |
| 306 | } catch (FileNotFoundException e) { |
| 307 | factoryFd = null; |
| 308 | } |
| 309 | |
| 310 | if (updateFd == null) { |
| 311 | if (factoryFd != null) { |
Abodunrinwa Toki | 6ace893 | 2017-04-28 19:25:24 +0100 | [diff] [blame] | 312 | return factoryFd; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 313 | } else { |
| 314 | throw new FileNotFoundException( |
| 315 | String.format("No model file found for %s", locale)); |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | final int updateFdInt = updateFd.getFd(); |
| 320 | final boolean localeMatches = Objects.equals( |
| 321 | locale.getLanguage().trim().toLowerCase(), |
| 322 | SmartSelection.getLanguage(updateFdInt).trim().toLowerCase()); |
| 323 | if (factoryFd == null) { |
| 324 | if (localeMatches) { |
Abodunrinwa Toki | 6ace893 | 2017-04-28 19:25:24 +0100 | [diff] [blame] | 325 | return updateFd; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 326 | } else { |
| 327 | closeAndLogError(updateFd); |
| 328 | throw new FileNotFoundException( |
| 329 | String.format("No model file found for %s", locale)); |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | if (!localeMatches) { |
| 334 | closeAndLogError(updateFd); |
Abodunrinwa Toki | 6ace893 | 2017-04-28 19:25:24 +0100 | [diff] [blame] | 335 | return factoryFd; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 336 | } |
| 337 | |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 338 | if (updateVersion > factoryVersion) { |
| 339 | closeAndLogError(factoryFd); |
Abodunrinwa Toki | 6ace893 | 2017-04-28 19:25:24 +0100 | [diff] [blame] | 340 | return updateFd; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 341 | } else { |
| 342 | closeAndLogError(updateFd); |
Abodunrinwa Toki | 6ace893 | 2017-04-28 19:25:24 +0100 | [diff] [blame] | 343 | return factoryFd; |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 344 | } |
| 345 | } |
| 346 | |
| 347 | @GuardedBy("mSmartSelectionLock") // Do not call outside this lock. |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 348 | private void destroySmartSelectionIfExistsLocked() { |
| 349 | if (mSmartSelection != null) { |
| 350 | mSmartSelection.close(); |
| 351 | mSmartSelection = null; |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | @GuardedBy("mSmartSelectionLock") // Do not call outside this lock. |
| 356 | @Nullable |
| 357 | private Locale findBestSupportedLocaleLocked(LocaleList localeList) { |
Abodunrinwa Toki | a2df6e5 | 2017-04-13 09:56:52 +0100 | [diff] [blame] | 358 | // Specified localeList takes priority over the system default, so it is listed first. |
| 359 | final String languages = localeList.isEmpty() |
| 360 | ? LocaleList.getDefault().toLanguageTags() |
| 361 | : localeList.toLanguageTags() + "," + LocaleList.getDefault().toLanguageTags(); |
| 362 | final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages); |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 363 | |
| 364 | final List<Locale> supportedLocales = |
| 365 | new ArrayList<>(getFactoryModelFilePathsLocked().keySet()); |
| 366 | final Locale updatedModelLocale = getUpdatedModelLocale(); |
| 367 | if (updatedModelLocale != null) { |
| 368 | supportedLocales.add(updatedModelLocale); |
| 369 | } |
| 370 | return Locale.lookup(languageRangeList, supportedLocales); |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 371 | } |
| 372 | |
| 373 | @GuardedBy("mSmartSelectionLock") // Do not call outside this lock. |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 374 | private Map<Locale, String> getFactoryModelFilePathsLocked() { |
Abodunrinwa Toki | c39006a1 | 2017-03-29 01:25:23 +0100 | [diff] [blame] | 375 | if (mModelFilePaths == null) { |
| 376 | final Map<Locale, String> modelFilePaths = new HashMap<>(); |
| 377 | final File modelsDir = new File(MODEL_DIR); |
| 378 | if (modelsDir.exists() && modelsDir.isDirectory()) { |
| 379 | final File[] models = modelsDir.listFiles(); |
| 380 | final Pattern modelFilenamePattern = Pattern.compile(MODEL_FILE_REGEX); |
| 381 | final int size = models.length; |
| 382 | for (int i = 0; i < size; i++) { |
| 383 | final File modelFile = models[i]; |
| 384 | final Matcher matcher = modelFilenamePattern.matcher(modelFile.getName()); |
| 385 | if (matcher.matches() && modelFile.isFile()) { |
| 386 | final String language = matcher.group(1); |
| 387 | final Locale locale = Locale.forLanguageTag(language); |
| 388 | modelFilePaths.put(locale, modelFile.getAbsolutePath()); |
| 389 | } |
| 390 | } |
| 391 | } |
| 392 | mModelFilePaths = modelFilePaths; |
| 393 | } |
| 394 | return mModelFilePaths; |
| 395 | } |
| 396 | |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 397 | @Nullable |
| 398 | private Locale getUpdatedModelLocale() { |
| 399 | try { |
| 400 | final ParcelFileDescriptor updateFd = ParcelFileDescriptor.open( |
| 401 | new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY); |
| 402 | final Locale locale = Locale.forLanguageTag( |
| 403 | SmartSelection.getLanguage(updateFd.getFd())); |
| 404 | closeAndLogError(updateFd); |
| 405 | return locale; |
| 406 | } catch (FileNotFoundException e) { |
| 407 | return null; |
| 408 | } |
| 409 | } |
| 410 | |
Abodunrinwa Toki | e0b5789 | 2017-04-28 19:59:57 +0100 | [diff] [blame] | 411 | private TextClassification createClassificationResult( |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 412 | SmartSelection.ClassificationResult[] classifications, |
| 413 | String text, int start, int end) { |
| 414 | final String classifiedText = text.substring(start, end); |
Abodunrinwa Toki | e0b5789 | 2017-04-28 19:59:57 +0100 | [diff] [blame] | 415 | final TextClassification.Builder builder = new TextClassification.Builder() |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 416 | .setText(classifiedText); |
Abodunrinwa Toki | 9b4c82a | 2017-02-06 20:29:36 +0000 | [diff] [blame] | 417 | |
Abodunrinwa Toki | a6096f6 | 2017-03-08 17:21:40 +0000 | [diff] [blame] | 418 | final int size = classifications.length; |
| 419 | for (int i = 0; i < size; i++) { |
| 420 | builder.setEntityType(classifications[i].mCollection, classifications[i].mScore); |
| 421 | } |
| 422 | |
Abodunrinwa Toki | 7d9b297 | 2017-03-14 21:56:31 +0000 | [diff] [blame] | 423 | final String type = getHighestScoringType(classifications); |
Abodunrinwa Toki | 46664a8 | 2017-12-11 17:02:06 +0000 | [diff] [blame] | 424 | addActions(builder, IntentFactory.create(mContext, type, classifiedText)); |
Abodunrinwa Toki | 54486c1 | 2017-04-19 21:02:36 +0100 | [diff] [blame] | 425 | |
Abodunrinwa Toki | 008f387 | 2017-11-27 19:32:35 +0000 | [diff] [blame] | 426 | return builder.setSignature(getSignature(text, start, end)).build(); |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 427 | } |
| 428 | |
Abodunrinwa Toki | ba38562 | 2017-11-29 19:30:32 +0000 | [diff] [blame] | 429 | /** Extends the classification with the intents that can be resolved. */ |
| 430 | private void addActions( |
| 431 | TextClassification.Builder builder, List<Intent> intents) { |
| 432 | final PackageManager pm = mContext.getPackageManager(); |
| 433 | final int size = intents.size(); |
| 434 | for (int i = 0; i < size; i++) { |
| 435 | final Intent intent = intents.get(i); |
| 436 | final ResolveInfo resolveInfo; |
| 437 | if (intent != null) { |
| 438 | resolveInfo = pm.resolveActivity(intent, 0); |
Abodunrinwa Toki | fafdb73 | 2017-02-02 11:07:05 +0000 | [diff] [blame] | 439 | } else { |
Abodunrinwa Toki | ba38562 | 2017-11-29 19:30:32 +0000 | [diff] [blame] | 440 | resolveInfo = null; |
| 441 | } |
| 442 | if (resolveInfo != null && resolveInfo.activityInfo != null) { |
| 443 | final String packageName = resolveInfo.activityInfo.packageName; |
| 444 | CharSequence label; |
| 445 | Drawable icon; |
| 446 | if ("android".equals(packageName)) { |
| 447 | // Requires the chooser to find an activity to handle the intent. |
| 448 | label = IntentFactory.getLabel(mContext, intent); |
| 449 | icon = null; |
| 450 | } else { |
| 451 | // A default activity will handle the intent. |
| 452 | intent.setComponent( |
| 453 | new ComponentName(packageName, resolveInfo.activityInfo.name)); |
| 454 | icon = resolveInfo.activityInfo.loadIcon(pm); |
| 455 | if (icon == null) { |
| 456 | icon = resolveInfo.loadIcon(pm); |
| 457 | } |
| 458 | label = resolveInfo.activityInfo.loadLabel(pm); |
| 459 | if (label == null) { |
| 460 | label = resolveInfo.loadLabel(pm); |
| 461 | } |
Abodunrinwa Toki | fafdb73 | 2017-02-02 11:07:05 +0000 | [diff] [blame] | 462 | } |
Abodunrinwa Toki | ba38562 | 2017-11-29 19:30:32 +0000 | [diff] [blame] | 463 | final String labelString = (label != null) ? label.toString() : null; |
Abodunrinwa Toki | ba38562 | 2017-11-29 19:30:32 +0000 | [diff] [blame] | 464 | if (i == 0) { |
Jan Althaus | 0d9fbb9 | 2017-11-28 12:19:33 +0100 | [diff] [blame] | 465 | builder.setPrimaryAction(intent, labelString, icon); |
Abodunrinwa Toki | ba38562 | 2017-11-29 19:30:32 +0000 | [diff] [blame] | 466 | } else { |
Jan Althaus | 0d9fbb9 | 2017-11-28 12:19:33 +0100 | [diff] [blame] | 467 | builder.addSecondaryAction(intent, labelString, icon); |
Abodunrinwa Toki | fafdb73 | 2017-02-02 11:07:05 +0000 | [diff] [blame] | 468 | } |
Abodunrinwa Toki | fafdb73 | 2017-02-02 11:07:05 +0000 | [diff] [blame] | 469 | } |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 470 | } |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 471 | } |
| 472 | |
Abodunrinwa Toki | d2d1399 | 2017-03-24 21:43:13 +0000 | [diff] [blame] | 473 | private static int getHintFlags(CharSequence text, int start, int end) { |
| 474 | int flag = 0; |
| 475 | final CharSequence subText = text.subSequence(start, end); |
| 476 | if (Patterns.AUTOLINK_EMAIL_ADDRESS.matcher(subText).matches()) { |
| 477 | flag |= SmartSelection.HINT_FLAG_EMAIL; |
| 478 | } |
| 479 | if (Patterns.AUTOLINK_WEB_URL.matcher(subText).matches() |
| 480 | && Linkify.sUrlMatchFilter.acceptMatch(text, start, end)) { |
| 481 | flag |= SmartSelection.HINT_FLAG_URL; |
| 482 | } |
Abodunrinwa Toki | d2d1399 | 2017-03-24 21:43:13 +0000 | [diff] [blame] | 483 | return flag; |
| 484 | } |
| 485 | |
Abodunrinwa Toki | 7d9b297 | 2017-03-14 21:56:31 +0000 | [diff] [blame] | 486 | private static String getHighestScoringType(SmartSelection.ClassificationResult[] types) { |
| 487 | if (types.length < 1) { |
| 488 | return ""; |
| 489 | } |
| 490 | |
| 491 | String type = types[0].mCollection; |
| 492 | float highestScore = types[0].mScore; |
| 493 | final int size = types.length; |
| 494 | for (int i = 1; i < size; i++) { |
| 495 | if (types[i].mScore > highestScore) { |
| 496 | type = types[i].mCollection; |
| 497 | highestScore = types[i].mScore; |
| 498 | } |
| 499 | } |
| 500 | return type; |
| 501 | } |
| 502 | |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 503 | /** |
Abodunrinwa Toki | 146d0d4 | 2017-04-25 01:39:19 +0100 | [diff] [blame] | 504 | * Closes the ParcelFileDescriptor and logs any errors that occur. |
| 505 | */ |
| 506 | private static void closeAndLogError(ParcelFileDescriptor fd) { |
| 507 | try { |
| 508 | fd.close(); |
| 509 | } catch (IOException e) { |
| 510 | Log.e(LOG_TAG, "Error closing file.", e); |
| 511 | } |
| 512 | } |
| 513 | |
| 514 | /** |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 515 | * Creates intents based on the classification type. |
| 516 | */ |
| 517 | private static final class IntentFactory { |
| 518 | |
| 519 | private IntentFactory() {} |
| 520 | |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 521 | @NonNull |
| 522 | public static List<Intent> create(Context context, String type, String text) { |
| 523 | final List<Intent> intents = new ArrayList<>(); |
Abodunrinwa Toki | a6096f6 | 2017-03-08 17:21:40 +0000 | [diff] [blame] | 524 | type = type.trim().toLowerCase(Locale.ENGLISH); |
Abodunrinwa Toki | 70d41cd | 2017-05-02 21:43:41 +0100 | [diff] [blame] | 525 | text = text.trim(); |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 526 | switch (type) { |
| 527 | case TextClassifier.TYPE_EMAIL: |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 528 | intents.add(new Intent(Intent.ACTION_SENDTO) |
| 529 | .setData(Uri.parse(String.format("mailto:%s", text)))); |
| 530 | intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) |
Richard Ledley | 68d9452 | 2017-10-05 10:52:19 +0100 | [diff] [blame] | 531 | .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) |
| 532 | .putExtra(ContactsContract.Intents.Insert.EMAIL, text)); |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 533 | break; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 534 | case TextClassifier.TYPE_PHONE: |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 535 | intents.add(new Intent(Intent.ACTION_DIAL) |
| 536 | .setData(Uri.parse(String.format("tel:%s", text)))); |
| 537 | intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) |
| 538 | .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) |
| 539 | .putExtra(ContactsContract.Intents.Insert.PHONE, text)); |
| 540 | intents.add(new Intent(Intent.ACTION_SENDTO) |
| 541 | .setData(Uri.parse(String.format("smsto:%s", text)))); |
| 542 | break; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 543 | case TextClassifier.TYPE_ADDRESS: |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 544 | intents.add(new Intent(Intent.ACTION_VIEW) |
| 545 | .setData(Uri.parse(String.format("geo:0,0?q=%s", text)))); |
| 546 | break; |
Abodunrinwa Toki | 9b4c82a | 2017-02-06 20:29:36 +0000 | [diff] [blame] | 547 | case TextClassifier.TYPE_URL: |
Abodunrinwa Toki | 86ef982 | 2017-05-11 20:05:50 +0100 | [diff] [blame] | 548 | final String httpPrefix = "http://"; |
| 549 | final String httpsPrefix = "https://"; |
| 550 | if (text.toLowerCase().startsWith(httpPrefix)) { |
| 551 | text = httpPrefix + text.substring(httpPrefix.length()); |
| 552 | } else if (text.toLowerCase().startsWith(httpsPrefix)) { |
| 553 | text = httpsPrefix + text.substring(httpsPrefix.length()); |
| 554 | } else { |
| 555 | text = httpPrefix + text; |
Abodunrinwa Toki | 70d41cd | 2017-05-02 21:43:41 +0100 | [diff] [blame] | 556 | } |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 557 | intents.add(new Intent(Intent.ACTION_VIEW, Uri.parse(text)) |
| 558 | .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName())); |
| 559 | break; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 560 | } |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 561 | return intents; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 562 | } |
| 563 | |
| 564 | @Nullable |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 565 | public static String getLabel(Context context, @Nullable Intent intent) { |
| 566 | if (intent == null || intent.getAction() == null) { |
| 567 | return null; |
| 568 | } |
| 569 | switch (intent.getAction()) { |
| 570 | case Intent.ACTION_DIAL: |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 571 | return context.getString(com.android.internal.R.string.dial); |
Jan Althaus | 92d7683 | 2017-09-27 18:14:35 +0200 | [diff] [blame] | 572 | case Intent.ACTION_SENDTO: |
| 573 | switch (intent.getScheme()) { |
| 574 | case "mailto": |
| 575 | return context.getString(com.android.internal.R.string.email); |
| 576 | case "smsto": |
| 577 | return context.getString(com.android.internal.R.string.sms); |
| 578 | default: |
| 579 | return null; |
| 580 | } |
| 581 | case Intent.ACTION_INSERT_OR_EDIT: |
| 582 | switch (intent.getDataString()) { |
| 583 | case ContactsContract.Contacts.CONTENT_ITEM_TYPE: |
| 584 | return context.getString(com.android.internal.R.string.add_contact); |
| 585 | default: |
| 586 | return null; |
| 587 | } |
| 588 | case Intent.ACTION_VIEW: |
| 589 | switch (intent.getScheme()) { |
| 590 | case "geo": |
| 591 | return context.getString(com.android.internal.R.string.map); |
| 592 | case "http": // fall through |
| 593 | case "https": |
| 594 | return context.getString(com.android.internal.R.string.browse); |
| 595 | default: |
| 596 | return null; |
| 597 | } |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 598 | default: |
| 599 | return null; |
Abodunrinwa Toki | 6b76675 | 2017-01-17 16:25:38 -0800 | [diff] [blame] | 600 | } |
| 601 | } |
| 602 | } |
Abodunrinwa Toki | 43e0350 | 2017-01-13 13:46:33 -0800 | [diff] [blame] | 603 | } |